[{"data":1,"prerenderedAt":997},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"tv-100-apps-100-hours":121,"tv-100-apps-100-hours-seasons":132,"tv-100-apps-100-hours-episodes":186,"sales-reps":745},{"items":4},[5,29,49,69],{"id":6,"title":7,"url":8,"page":8,"children":9},"522e608a-77b0-4333-820d-d4f44be2ade1","Solutions",null,[10,15,20,25],{"id":11,"title":12,"url":8,"page":13},"fcafe85a-a798-4710-9e7a-776fe413aae5","Headless CMS",{"permalink":14},"/solutions/headless-cms",{"id":16,"title":17,"url":8,"page":18},"79972923-93cf-4777-9e32-5c9b0315fc10","Backend-as-a-Service",{"permalink":19},"/solutions/backend-as-a-service",{"id":21,"title":22,"url":8,"page":23},"0fa8d0c1-7b64-4f6f-939d-d7fdb99fc407","Product Information",{"permalink":24},"/solutions/product-information-management",{"id":26,"title":27,"url":28,"page":8},"63946d54-6052-4780-8ff4-91f5a9931dcc","100+ Things to Build","https://directus.io/blog/100-tools-apps-and-platforms-you-can-build-with-directus",{"id":30,"title":31,"url":8,"page":8,"children":32},"8ab4f9b1-f3e2-44d6-919b-011d91fe072f","Resources",[33,37,41,45],{"id":34,"title":35,"url":36,"page":8},"f951fb84-8777-4b84-9e91-996fe9d25483","Documentation","https://docs.directus.io",{"id":38,"title":39,"url":40,"page":8},"366febc7-a538-4c08-a326-e6204957f1e3","Guides","https://docs.directus.io/guides/",{"id":42,"title":43,"url":44,"page":8},"aeb9128e-1c5f-417f-863c-2449416433cd","Community","https://directus.chat",{"id":46,"title":47,"url":48,"page":8},"da1c2ed8-0a77-49b0-a903-49c56cb07de5","Release Notes","https://github.com/directus/directus/releases",{"id":50,"title":51,"url":8,"page":8,"children":52},"d61fae8c-7502-494a-822f-19ecff3d0256","Support",[53,57,61,65],{"id":54,"title":55,"url":56,"page":8},"8c43c781-7ebd-475f-a931-747e293c0a88","Issue Tracker","https://github.com/directus/directus/issues",{"id":58,"title":59,"url":60,"page":8},"d77bb78e-cf7b-4e01-932a-514414ba49d3","Feature Requests","https://github.com/directus/directus/discussions?discussions_q=is:open+sort:top",{"id":62,"title":63,"url":64,"page":8},"4346be2b-2c53-476e-b53b-becacec626a6","Community Chat","https://discord.com/channels/725371605378924594/741317677397704757",{"id":66,"title":67,"url":68,"page":8},"26c115d2-49f7-4edc-935e-d37d427fb89d","Cloud Dashboard","https://directus.cloud",{"id":70,"title":71,"url":8,"page":8,"children":72},"49141403-4f20-44ac-8453-25ace1265812","Organization",[73,78,84,88],{"id":74,"title":75,"url":76,"page":77},"1f36ea92-8a5e-47c8-914c-9822a8b9538a","About","/about",{"permalink":76},{"id":79,"title":80,"url":81,"page":82},"b84bf525-5471-4b14-a93c-225f6c386005","Careers","#",{"permalink":83},"/careers",{"id":85,"title":86,"url":87,"page":8},"86aabc3a-433d-434b-9efa-ad1d34be0a34","Brand Assets","https://drive.google.com/drive/folders/1lBOTba4RaA5ikqOn8Ewo4RYzD0XcymG9?usp=sharing",{"id":89,"title":90,"url":8,"page":91},"8d2fa1e3-198e-4405-81e1-2ceb858bc237","Contact",{"permalink":92},"/contact",{"items":94},[95,101,107,113],{"id":96,"title":97,"url":8,"page":98,"children":100},"8a1b7bfa-429d-4ffc-a650-2a5fdcf356da","Cloud Policies",{"permalink":99},"/cloud-policies",[],{"id":102,"title":103,"url":81,"page":104,"children":106},"bea848ef-828f-4306-8017-6b00ec5d4a0c","License",{"permalink":105},"/bsl",[],{"id":108,"title":109,"url":81,"page":110,"children":112},"4e914f47-4bee-42b7-b445-3119ee4196ef","Terms",{"permalink":111},"/terms",[],{"id":114,"title":115,"url":81,"page":116,"children":118},"ea69eda6-d317-4981-8421-fcabb1826bfd","Privacy",{"permalink":117},"/privacy",[],{"description":120},"\u003Cp>A composable backend to build your Headless CMS, BaaS, and more.&nbsp;\u003C/p>",{"id":122,"title":123,"logo":124,"cover":125,"tile":126,"announcement_text":8,"description":127,"slug":128,"one_liner":129,"card_text":8,"status":130,"sort":131},"fb9d1e83-52ea-4ab2-9ef9-23309f9b955d","100 Apps In 100 Hours","e6bba7c4-2ae2-4798-9ee9-d3b6a043b09e","d86c91af-e1e4-4f2f-9908-9fa6e322bb4c","fb0f9d45-be21-4634-94d4-2ef1cc5146f2","Follow Bryant as he embarks on missions to speed-build apps using Directus, within a tight limit of just 60 minutes. Armed with Directus and any other essential tools, his goal is to either fully develop the app's core functionality, or make significant progress, within the allotted hour.","100-apps-100-hours","One project. Just sixty minutes on the clock. Will Bryant get it built? Tune in to find out.","published",1,[133,141,156,171],{"id":134,"number":135,"show":122,"year":136,"episodes":137},"9095d401-66bc-416a-997b-f038365ce5ed",4,"2026",[138,139,140],"2c5b1e3e-c12f-4dc5-aef8-52082d41c192","8213071c-6d2a-4564-b058-f177900dcad1","6352abf0-f785-4887-9ee8-44553ea3b55e",{"id":142,"number":143,"show":122,"year":144,"episodes":145},"d6b229fe-38fc-495b-ba0c-c574ebfea38f",3,"2025",[146,147,148,149,150,151,152,153,154,155],"ec88bef1-fffd-43eb-9d93-3123dc381b97","ab550907-1a5a-4fc0-8208-764465a1864f","97806b9d-063a-447f-9dca-617217ae5879","2a920d49-91e0-4237-bbd7-12e1181ad02a","fee34ecb-a4b6-4218-ac25-d15052c7604d","1ac7647d-0f09-4698-8ca0-e4448a4fe0e9","54789ca9-ca1d-4c25-a4d7-31c32de53d5d","09a64df3-79d6-4ede-a394-30a2499c5fee","bfb4b33c-1494-4111-8f33-d59bfde9df65","36839053-13e7-4eb6-8e73-efb73bf61fb1",{"id":157,"number":158,"show":122,"year":159,"episodes":160},"14fda5f2-95de-4dbe-a4e2-3fd956c21c19",2,"2024",[161,162,163,164,165,166,167,168,169,170],"9a3a8ffa-a27b-421c-93cf-3da2dcb726e9","d072a935-906e-4208-a5dc-e9b117d0ab29","b9f1d4cf-f53c-49db-9e87-adf7e3b9ff99","aad8d674-2b58-4604-8e43-b98f7c6e05cb","6bff0c09-ad87-4d5c-b227-89b8c3c02220","6fb9aa9a-2b59-44b6-b78f-d1831fa657c6","620cf225-a23a-415a-ad95-9ba8e2dec984","b8b36125-7a4a-40e4-85f6-f4fe9138085e","385bdd7d-038d-4f9c-8037-357e5272420a","383c24d5-b6b5-4d66-aba6-6997af5f77b4",{"id":172,"number":131,"show":122,"year":173,"episodes":174},"56dda5ff-2c3a-41ce-ae3a-580d6101026b","2023",[175,176,177,178,179,180,181,182,183,184,185],"cb4e067f-9507-4e18-ab9a-435565f9e653","8434838a-8e4f-489a-8da2-fbf10de5de6a","c997b25e-400c-4350-bba4-f63853d844f7","1109be0d-8ab5-479b-a052-8ad30d9ffb1c","dcb952e0-7d13-43ea-9b1c-f2ca63efd07d","0aae287d-3916-4d91-9310-25828998e562","8fed0eee-43f6-4767-8b77-79da7a059821","a311b57d-34e7-4073-9cf3-6a8c6c0f8b85","9271a4fc-addf-4400-9591-f2f2ec07bd79","30c48566-52cd-4728-a633-ca9675acc959","c067c012-5fe1-4d70-8946-8bfc8e1b22c0",[187,214,230,246,262,280,298,315,334,351,368,397,413,429,444,459,474,489,504,519,535,550,566,580,595,609,624,638,653,667,681,695,713,728],{"id":175,"slug":188,"vimeo_id":189,"description":190,"tile":191,"length":192,"resources":193,"people":197,"episode_number":131,"published":201,"title":202,"video_transcript_html":203,"video_transcript_text":204,"content":8,"seo":205,"status":130,"episode_people":206,"recommendations":208,"season":212},"expenses-system","893800625","Expensify is an expense management tool that tracks receipts, rolls them up into business reports and documents, and handles the approval workflow. Bryant has one hour to build it (or die trying).","48dba4a7-4bf0-45df-9b3d-4de5ffa8b98a",63,[194],{"name":195,"url":196},"Mindee OCR API","https://www.mindee.com/",[198],{"name":199,"url":200},"Bryant Gillespie","https://directus.io/team/bryant-gillespie","2023-12-22","Mission: Expensify Clone","\u003Cp>Bryant Gillespie: Hi. Bryant here. Welcome to the inaugural episode of 100 apps in 100 hours, where I will take some of your favorite apps and try to recreate them in under 1 hour or die trying. Hopefully not dying, but you get the idea. And first on our list today is Expensify.\u003C/p>\n\u003Cp>So we're going to build an Expensify clone. Expensify is expense management. And let's actually I'm not really sure I like the word clone here. Let's call this, a tribute, like the Tenacious d song. This is just a tribute because it would be very difficult to build all the functionality of Expensify in 1 hour.\u003C/p>\n\u003Cp>Obviously, they've built an entire business and they probably have thousands of developers, but let's get as close as we can. So when I think about Expensify, let's just go to their website, I think of the the first thing I think of is scanning the receipt, taking a picture of it, and it does the automagic stuff, where it automatically fills out expense reports for me, which, to be honest, is kind of a pain in the rear anyway. So, that is one thing that we're going to be concerned with, the let's just map this out. Automagic Receipt Management. We're going to have expense reports.\u003C/p>\n\u003Cp>Expense reports. So we wanna roll up all of those expenses, and, some of those are gonna be billable, And all those may be reimbursable, so we need to track for that. What else? We want to be able to approve expense reports. Seems like a good list for getting started.\u003C/p>\n\u003Cp>Alright, so I'm gonna pull up my handy tool of choice, Directus. So Directus, for those of you who aren't familiar with it, is a back end toolkit that allows you to build pretty much anything or everything, and get instant APIs on top of any SQL database. That's the pitch. I swear that's it. Let's actually run the clock, and I'm late on the clock here, but we'll start the clock 60 minutes on the timer.\u003C/p>\n\u003Cp>Let's go. We're gonna hop into Directus, and I am in my data model settings right now. And I'll zoom way in so you guys can actually see this alright. First thing we're gonna do is let's spec out our expenses. So let's just dive into our data model.\u003C/p>\n\u003Cp>We're gonna have expenses as one of our collections, so Directus is gonna create this table for us. And when we go to the optional system fields, we'll probably have a status. Let's go with the sort for the order on the expense report and we'll just select all these system fields because they have some presets that are useful to us. Alright, so we've got expenses, and I'm just gonna use a Expensify attribute. We'll create a folder here for this.\u003C/p>\n\u003Cp>We even give it like a nice green color. Boom. We're gonna organize all that inside our Expensify attribute. Okay. So the next thing, we are going to have an expense report.\u003C/p>\n\u003Cp>Right? We've got to roll up all those expenses somewhere. This is where we'll do it. Okay. Great.\u003C/p>\n\u003Cp>Okay. What else are we going to need? Let's just get started with this functionality. Alright. So, we go back to Expensify and let's try to figure out exactly what their data model looks like.\u003C/p>\n\u003Cp>My favorite way of doing that, short of interacting with the app is searching for their API. So we'll just go to the Expensify API, we'll go to their API reference, we'll just zoom way in on this, Expensify integration server, all you need to integrate. Alright. So request formats, we see what's going on here. Let's look for read and get policy lists.\u003C/p>\n\u003Cp>Maybe create. Okay. I see here's the expense creator. Alright. So, okay, it looks like here's a definition of the expense object.\u003C/p>\n\u003Cp>So reading through this, we've got a merchant, we've got the date of the expense, we've got the amount incense, we've got the currency, that's an integer, that's probably on purpose, three letter code of the expense, external ID, there's a category, there's a tag, is it billable, is it reimbursable, Comments, reports, IDs, tax. Okay. Straightforward enough, let's dive in. We'll go to our expenses and first, let's add our merchant. So we'll just create a new field inside our data model for Directus.\u003C/p>\n\u003Cp>This is just going to be a string type and clear enough, we are going to require value. We've got to have somebody that we got this receipt from. Let's change that to half width. And now let's move on to the amount. So let's do an input field.\u003C/p>\n\u003Cp>Now I could do this as an integer in cents. What I'm going to do instead is just use a decimal for this. So we'll do a decimal, I could change the scale to 2 points and, looks good. Alright. So now we've got the amount for the expense, we need to add a date for the expense.\u003C/p>\n\u003Cp>Expense date, let's just call that. So we'll choose the date time here and I'm gonna pick the time stamp as the type just because that will respect any time zones. I'm in the US. I use the 24 hour format. Or let's not use the 24 hour format since I'm in the US.\u003C/p>\n\u003Cp>No need to include the seconds. That's great. So we got our merchant. We got the amount. We got the expense date.\u003C/p>\n\u003Cp>Let's just do the name of the expense. Is that inside Expensify? Let's take a look. Created amounts, optional tag. Yeah.\u003C/p>\n\u003Cp>I'm just gonna give a name for the expense. Maybe we have some notes for that. Great. We'll add the text area field for that. We don't need any formatting.\u003C/p>\n\u003Cp>Great. Merchant amount, let's do a little cleanup on this. Directus makes it so easy to build apps that look great for your users. Alright. What else do we have?\u003C/p>\n\u003Cp>An expense comment. Got that. So then we have things like, is this billable? Is this reimbursable? So these are going to be toggles inside Directus.\u003C/p>\n\u003Cp>That's the name of the interface as we call it. Let's do is billable. I like to prefix those with is, just keeps things organized. Default value will be false, and looks good. Maybe we want to embellish this a little bit.\u003C/p>\n\u003Cp>So if you go into the advanced mode inside Directus, we can add notes for our users. Is this expense can we invoice this to a client? Great. Alright. Enabled.\u003C/p>\n\u003Cp>Let's just call it billable for the label. That seems a little better. The display is where it will show out show up in some of the different layouts inside Directus when you're looking at a list of records. Yeah. I don't see any need to change this.\u003C/p>\n\u003Cp>Great. Alright. We'll shrink that to half width. And I'm just gonna duplicate this one, and you can see how easy it is to build this out. Is reimbursable.\u003C/p>\n\u003Cp>Directus makes a a lot of this quick and painless. So we're gonna keep this on the expense collection. Is reimbursable. Yes. This is a reimbursable expense.\u003C/p>\n\u003Cp>We've got the user that created this, but, I wonder if there's a a use case where I have to expenses that somebody else submitted because they didn't do their job. So let's just add a owner, an expender. What is the the proper term for that? Buyer? Let's just call it an owner.\u003C/p>\n\u003Cp>Expense owner makes sense to me. That's what we'll call it for now. That is going to be our directus users collection. So that'll be the users of our app. Great.\u003C/p>\n\u003Cp>That's what we'll call it. That's the hardest thing in development is actually naming things. Okay. So this looks pretty good. This is a good start.\u003C/p>\n\u003Cp>What else are we missing for this? We've got a category, we've got the tags, Name of the tags. Yeah. We could we could do that. Let's let's build in a little more functionality.\u003C/p>\n\u003Cp>So we'll do tags. Maybe I wanna do tags and categories as a separate collection, and actually build those into the relationships. So category to me, there's only one single category that an expense can be in. Tags obviously could be many. So here we are going to use a many to 1.\u003C/p>\n\u003Cp>So this one, expense belongs to a single category. That's what we're gonna call it. Let's call it expense category. Great. And for our related collection, let's also do expense categories as the name of that.\u003C/p>\n\u003Cp>Now, I could click Save and Directus will do some magic for me, or I can open up advanced mode and go in and actually look at the details of this relationship. So if we look, we can see we've got this collection, we've got an expense category field, and Directus is telling me that, hey, we're gonna create a new collection called expense categories and we're gonna use the ID field of that to populate. Alright. The mini to 1, I could control the display template, but for now I don't have any other fields because that, collection is yet to be created. But we'll go in and go ahead and create this field here.\u003C/p>\n\u003Cp>And if I were to go back to my data model as well, we'll see that Directus created that collection for us. So now we've got our expense category. Let's go in and do tags as well. So tags are going to be a many to many relationship inside Directus. One tag could be applied to many different expenses, and many expenses could have many different tags.\u003C/p>\n\u003Cp>Great. You didn't know we were getting into data model expertise today, but let's name this thing. What are we gonna call it? We're just gonna call it tags. Right?\u003C/p>\n\u003Cp>We're going to create a related collection called expense tags and what else do we need to do? Show a link to the item so we can edit that tag as needed. Great. Save. Alright.\u003C/p>\n\u003Cp>So now we've got the basis of our expenses. Let's go in and flesh out these other ones that we just created. For expense categories, we're probably gonna have a name for that expense category. And, you know, maybe we got, like, an account number for our profit and loss, account name. Cool.\u003C/p>\n\u003Cp>Alright. And then for our tags, let's go in and add name and maybe we want to create a color for our tags. So I'm just gonna search for the color field here inside Directus. We'll give each tag a color. I could go through and preset these.\u003C/p>\n\u003Cp>If you leave this blank, Directus will actually create, give you a few of those presets that look nice anyway. Alright. So always gotta do my proper formatting. This is kind of a speed run, but one of the other things that I am a little OCD about, honestly, is cleaning things up and having a nice row of or nice columns of icons inside the app. So you can see a website project I've got working on here, but, you know, for now let's just roll with this.\u003C/p>\n\u003Cp>So we've got our tags, we've got our categories. The last thing that we need to do, if we open up one of our expense reports, which we have no data in that. We need to figure that one out. But we need a link back to our expense reports. So let's go into the data model.\u003C/p>\n\u003Cp>We'll go to expense reports. And this is really nice. We just recently added this search feature inside the data model, which is great, especially when you have hundreds of collections. So we'll go into our expense report. We're going to have a name.\u003C/p>\n\u003Cp>Do we even need a name? Yeah. We'll give it a name of the expense report. This could be a title. Bryant's second quarter expenses, blah blah blah, etcetera.\u003C/p>\n\u003Cp>Alright. Then we have a owner or could be like submitted by, you know, that's probably better but we stuck with owner on the other one so we'll just roll with that. For our related collection, we're gonna do direct us users. And you could pick these from a drop down over here on the right just by clicking those, but if you'll notice when I hit on one of the actual collections inside Directus, this turns purple just to let me know I'm doing the right thing here. So we got the owner of this expense report, yeah, I don't like that terminology, but I I don't wanna backtrack at this point.\u003C/p>\n\u003Cp>Date created. Alright. What else do we have? Then we're gonna need the actual expenses on this. So in this case, we've got a one to many relationship.\u003C/p>\n\u003Cp>So one expense report has many expenses. We're gonna call the key for this expenses, that's what it's gonna be stored as a column in the database as, and then we're going to pick our expenses as the related collection. And for the foreign key, we don't have one that exists already, but Directus will create one for us, which is nice. We're just gonna call it expense report because there is a single report it is linked to. And then I could go in and use the let's change this to a table view.\u003C/p>\n\u003Cp>We could see the merchant. Maybe we want to add the expense date, the amount, and, maybe the category. So here when it's a related collection I can just pick up the the related fields as well. Great, enable searching and filtering, hit save and it's probably good enough for now, right? Let's dive in and actually start creating some expense reports.\u003C/p>\n\u003Cp>So, I've got a new expense report. Brent's 4th quarter, it's not even the 4th quarter, 3rd quarter, let's just call it September expenses. Expenses. Great. Alright.\u003C/p>\n\u003Cp>So we go in, I'm gonna be the owner of this. I am not a user at this point, so I'll just use mister hop along. And now we can go in and add some of our expenses. Alright. So we've got Starbucks.\u003C/p>\n\u003Cp>The expense date is going to be this morning at, what, 7:30. I'm in West Virginia of all places, so we don't have a a ton of coffee options here. I know some people are gonna be mad that we've got Starbucks in here. That was expensive. So let's say 2102 has the amount.\u003C/p>\n\u003Cp>Coffee Needed that real bad. Alright. Is this billable? Probably not. Is this reimbursable?\u003C/p>\n\u003Cp>You know, we could have this where I could select this, but one of the other things you could do inside Directus is preset this to a default setting for, like, the the current user, for example. So for now, I'll just pre we'll pick hops along. We don't have any expense categories. I wanna say this would probably fall under meals and entertainment or just meals. No, you wanna divide those out.\u003C/p>\n\u003Cp>I hate doing bookkeeping, so accounting account number, I wanna say it's like the 3 or 4 100. Mills Entertainment. Great. Cool. Keeping track of all that.\u003C/p>\n\u003Cp>Tags. What are we gonna tag this as? Coffee would be the name of it. I don't really like any of these. Maybe we can pick, yeah let's just go with red for coffee.\u003C/p>\n\u003Cp>That's great. Cool. Alright. So we've got our information here. We've added our first expense.\u003C/p>\n\u003Cp>Pretty great. Cool. We've got our expense report. Yeah. The next thing maybe we want to fire off this expense report to somebody on our team.\u003C/p>\n\u003Cp>How are we doing on time? We're at 43 minutes, so that took about, 18, 20 minutes to flesh out this. How can we send this expense report to a team member, for approval? Well, first of all, let's do an expense report. Let's add an approved by.\u003C/p>\n\u003Cp>So we wanna track who this was approved by. That's gonna be a directus users, so it's gonna be one of our users. Great. And then maybe an approval date, Right. So we'll do a date timestamp, date approved.\u003C/p>\n\u003Cp>Great. No 24 hour format. So now we got 2 fields to track our approvals. Let's go in and use the flows inside Directus to send this report off or send a link to somebody to this report. So send expense report for approval.\u003C/p>\n\u003Cp>Great. And then we can even customize this. Let's do send. Yeah. There we go.\u003C/p>\n\u003Cp>Looks great. Scheduled send. We'll make this orange just so it stands out. Alright, so now I've got a list of different triggers. I want to actually trigger this one manually.\u003C/p>\n\u003Cp>So Directus gives us the option within a specific collection on the layout page or the detail page to send this off. So we will go in and look for expense reports. Okay. There's my expense report. Great.\u003C/p>\n\u003Cp>And then I could choose whether this is asynchronous. So, you know, do I wanna do this in the background? What are the locations where I can trigger this? So we wanna do on the collections and items pages. That's fine.\u003C/p>\n\u003Cp>We'll do both of those. If you are on the collection page, this will require a selection. We we do need you to pick this to actually send this off. And then next I'm gonna do this require confirmation because I get the ability to add different fields to the system, to prop for those fields when I send it. So I am going to add a new field for approver.\u003C/p>\n\u003Cp>So who do we need to send this? Approver. Who needs to approve this report. Great. Now the type I'm gonna pick here is gonna be JSON because we have a special little interface called the collection item drop down where I can pick a value from this and store it in JSON.\u003C/p>\n\u003Cp>So here again, we're gonna do directus users, and I'm gonna pick the user that I want to approve this report. And I could go in and as far as customizing the display template as well. You know, let's do the avatar, first name, last name. Great. And then maybe we even wanna add a note.\u003C/p>\n\u003Cp>Note. Close note to approver and that'll be just a simple string. No big deal. Great. Got the field width, let's make it full width.\u003C/p>\n\u003Cp>Actually, let's do a text area for that. Just a quick note. Great. Okay. So now that we've got that, what I'm gonna do, I'm gonna save this and I just wanna get a look at what it's gonna look like when I'm on my expense report.\u003C/p>\n\u003Cp>So over here on the right hand side, you can see Send Expense Report for Approval. I can select items or if I'm inside this expense report, I can click expense report, and then I get this confirmation dialogue of who needs to approve this, Matt. My note to Matt is going to be, hey, I need more budget for these videos. Great. Run flow on current item and let's just see what that came up with.\u003C/p>\n\u003Cp>So inside my flows, I can go in and check my logs and I could see what was actually sent down the pipe. So here's the trigger, here's the body of the payload, and this is actually what we received. This is what we will use to fire off that message. Alright. So this looks great.\u003C/p>\n\u003Cp>What I'm going to do, I'm just gonna copy this information, this is what I need, and I open up Versus Code. Let's start with a new window here. Where are you? New window. New file.\u003C/p>\n\u003Cp>Alright. So this is our trigger data. Great. Perfect. Okay.\u003C/p>\n\u003Cp>So now let's build the steps of our flow. We wanna send this expense report for approval, but we need an email to send it to, and we do not have that data here. So we need to actually get that data. So that's gonna be our next step. Get approval approver email is what we'll call it.\u003C/p>\n\u003Cp>And we will click read items from the database. The permissions from the trigger should be fine. I can also change this to full access if this is gonna be, triggered from somewhere other than inside the app. And for the collection we are going to use the directus users, but you can see this is not coming up in the search because it's a system collection. So I'm just gonna go in and hit edit raw value and we will do directus underscore users.\u003C/p>\n\u003Cp>That's our collection. Now for the IDs, we are going to use our little squarely bracket mustache syntax and we are gonna pick up the ID from this approver key that we selected. Alright, so, how do I do split screen here? Fancy fancy. Alright.\u003C/p>\n\u003Cp>So this is gonna be located at, so we use our brackets, mustache, we're gonna do dollar sign trigger. Trigger is the only one that has a special syntax. All the other keys of your operations get appended under, the key that you set here without the dollar sign. That's one of the big things that I see a lot of people get tripped up on. And it's gonna be dot body dot approver.key.\u003C/p>\n\u003Cp>Great. That's gonna be our ID. Do we need a query for this? I don't think so because we're picking up the key there. Let's hit save and then next we want to, I'm I'm just gonna save this.\u003C/p>\n\u003Cp>Let's run that one more time. So we'll go back to our expense reports. I'll pick Matt here. Hey, dude. We'll go back to our flow, and this is a good way of building flow so I can actually see the details of what we've got going on.\u003C/p>\n\u003Cp>Now I can see an issue right away. This for this demo anyway. This is gonna send to matt@example.com, which is not a real email address. So I'm just gonna quickly change this to one of my own emails just for demonstration purposes and to prove that we can actually build this app this quickly. Alright, we'll go back to our flows.\u003C/p>\n\u003Cp>Now let's actually send that email, and again when working with flows it's helpful to have like a code editor open, but, this case is fairly straightforward. We are going to send an email. We'll choose the send email operation and for the addresses we're gonna use that squirrely mustache syntax again. This is going to be get, this is where my memory struggles. This is called git approver email.\u003C/p>\n\u003Cp>So the 2 is gonna be git_approveremail, and that's gonna be the user object. So we're just gonna do email. I have to hit enter here to save this. We could also add additional emails if I wanted to if I wanted to cc myself. Let's call this expense reports.\u003C/p>\n\u003Cp>And the type here could be WYSIWYG. You can even choose a template if you are using our custom extensions, but let's go in do this as a WYSIWYG will be fine and we're going to do the trigger. So we we just want to pick up the content from our notes, which is gonna be body dot note. Should be Great. Alright.\u003C/p>\n\u003Cp>So this should populate that message into our WYSIWYG editor when we send this email out. Fingers crossed we're gonna save this. Let's test it out and see. How we doing on time? Alright.\u003C/p>\n\u003Cp>So we got about 32 minutes left, maybe a little less than that because I was, failing on throwing together the or I was failing to do the the countdown correctly. But, let's give this a shot. We'll go in, we'll send the expense report for approval, and we're going to send it to Rabat Miner. I need some more lattes. Alright.\u003C/p>\n\u003Cp>So we fire that off. Let's go back in and check our logs just to make sure. And it looks like the email went out. I'm just gonna open up my email inbox and voila, it it sent this out. That's great, that's all good, but, where's the actual expense report?\u003C/p>\n\u003Cp>There's no link. So let's go back in and what I'm gonna do, I'm just gonna capture the URL for this from my directus admin. Great. And then we're gonna go into our flow. We'll go back to that email and we just wanna give Matt like a button or a link or something here.\u003C/p>\n\u003Cp>So we're gonna do view and approve report. We'll enter in this email and we're gonna use our bracket mustache syntax and we'll do the trigger dot body dot keys 0. So the first item of the keys array is what we want to include. Cool. Great.\u003C/p>\n\u003Cp>Gravy. Save. Save. Alright. So now let's go back in.\u003C/p>\n\u003Cp>We'll send this expense report one last time, one more time with a link, hopefully. Fingers crossed, right? Alright, so now we wait and boom, we have a link. We click on the link, it logs us in and, Matt should be logging in to update the status on this. So we may want to go in and update the statuses for these.\u003C/p>\n\u003Cp>Let's do our expense reports. Let's change the status, draft. We'll do submitted for 1. Submitted. So submitted for approval.\u003C/p>\n\u003Cp>Approved. Maybe not approved is 1. Rejected. Let's call it rejected. That'll that'll be fun.\u003C/p>\n\u003Cp>That's a little harsh for an expense report, like, hey, your expense report was rejected. But, yeah, no worries. Oh, I goofed that up, didn't I? Should've saved before I started slamming away on the keyboard. Submitted.\u003C/p>\n\u003Cp>Alright. We'll just clean this up. Draft. Okay. Approved.\u003C/p>\n\u003Cp>Rejected. Alright. Let's just show the raw value for now or maybe a formatted value. That's fine. We'll do like a border, make it fancy looking, allow other values, no, allow no selection, no, for the interface though maybe it could be interesting if we do radio buttons.\u003C/p>\n\u003Cp>Great. Alright. Cool. So now we go in. Matt can approve this expense report and save it.\u003C/p>\n\u003Cp>But as you can see, I'm logged in as myself. I could potentially approve this expense report myself, which is is not cool. So we could use the roles and permissions inside Directus for that, where I could go in and say a given role like if we just had one called team member for example, I could go in and for our expense reports, I could go in and give custom permissions for this so that I cannot edit a certain field like the status. So this would prevent that user from actually, I'm sorry. I did that backwards.\u003C/p>\n\u003Cp>I would check all the other fields like the that user can update everything else about this expense report except the status or any other items that were that I needed. And likewise, one of the other things that I could do here is when I create a report, if we use the custom permissions for this, you could go in and define the presets where you can say, hey the current user is the default for this particular field. Sounds great. Alright. That is basic functionality of this.\u003C/p>\n\u003Cp>We've got the ability to add expenses, submit expense reports. You know, you might wanna try and tag into some other system to pay out these expenses, but for now I could stroke a check for those, I guess. But that leads us back to, boom boom boom, the automagic receipt management. So this part is a bit scary scary to me. I don't know how we're gonna do this in 26 minutes.\u003C/p>\n\u003Cp>Actually, I'm lying to you. I do know, what we're going to try because I've already cheated a little bit. Let's get that clock out of the way. I'm gonna make this full screen again. I have found this other service called Mindy Mindy?\u003C/p>\n\u003Cp>Mindy? Not sure what it's called, but it takes the receipts OCR, optical character recognition magic on them and apparently spits back out some data. Now, for the sake of time, all I've done is logged into this app and, set up an account. I haven't done anything else with it, so you're almost as fresh as I am at this point. I did see just a quick little tour tooltip that told me this is the API that I want.\u003C/p>\n\u003Cp>So let's take a look at at how we could do this. Anytime I upload an expense that has an image attached to it, and Directus is mobile friendly, so I could potentially log into this from my phone and update these expenses as well, just to to prove it to you there. Anytime I update one of those expenses, it has an image attached to it. We wanna send this off to this service and hopefully, I'd, like, not have to deal with actually entering in all the separate fields. Great.\u003C/p>\n\u003Cp>So first thing we need to do is, we got our expenses, let me close this one. We need to add an image field to upload the expense. So we'll go into our expenses. Let's add a image, receipt image or receipt file or we could just call it file. No need to get fancy with it.\u003C/p>\n\u003Cp>Alright. So now we've got the ability to upload a file. Let's start building a flow and then we'll incorporate this other service. So we've got, automagic COCR. Cool.\u003C/p>\n\u003Cp>Alright. So we'll do that on a vinthook. Let's do a action non blocking, so we don't wanna block the thread. Anytime a new item is created inside expenses, and we'll start there. Right?\u003C/p>\n\u003Cp>Alright. So let's go into, let's actually go back to our expenses. Let's just call this a new expense. Actually, we're not gonna do any of that. All I wanna do here and maybe we move this file up to the start actually.\u003C/p>\n\u003Cp>Let's move our file way up here. Alright. So the ideal scenario here is all I have to do create a new expense, upload an image of that, and then this service and Directus would do the rest for me. So let's just Google receipt images. Let's get a receipt image, see if we can find one that's pretty gnarly as well.\u003C/p>\n\u003Cp>What's this guy? Tesco Metro. Yeah. This one looks pretty beat up. This will be a good test, right?\u003C/p>\n\u003Cp>Save this image. Where did I go? Too many different tabs going on. So here's our Tesco shopping receipt. We hit save.\u003C/p>\n\u003Cp>Merchant value can't be null. That was a problem of mine, maybe I should go in and remove that, but you could set that up where it wasn't required. But now, if we take a look at our flow, that should have triggered our flow, we've got a payload, we can see here's the file that we're going to potentially pick up. Great. One of the other things that I wanna make sure, because we're gonna have to send this file to that service, is I'm gonna check our system collection and make sure that direct us files has public read permissions, and I could drill down to where, you know, only, files within a certain folder are available if I wanted to get very specific for this.\u003C/p>\n\u003Cp>Let's just leave it as is. Again, this is an Inexpanify Tribute, not a clone. Great. Alright, so now we've got our data coming from our new expense, we are going to wanna pass that to the mindd API. So let's see the API here.\u003C/p>\n\u003Cp>Alright. Load some stuff up for me. Alright. If I was smart I would have gone in here and cheated a little bit more than what I'm doing now. Alright.\u003C/p>\n\u003Cp>So I don't have an API key. How do I create an API key? API keys. Alright. We'll create an API key.\u003C/p>\n\u003Cp>We're gonna call this Directus. Great. Copy that API key. Don't you guys steal my API key? So So we'll go back to the API, go back to our documentation.\u003C/p>\n\u003Cp>Here's everything we're gonna need. Now is a good time to do the split screen stuff with, Arc. Great browser, still struggling, like 6 months on to figure this thing out. Alright, give myself a little more room. So we got the post request.\u003C/p>\n\u003Cp>We are going to use the send to, let's just call it OCR receipt. We are going to use the webhook request URL. We're gonna do a post request as it says. Here is our URL that we're gonna pick up. Hopefully, we could just copy that.\u003C/p>\n\u003Cp>Savvy. Of course it takes that post request. This looks okay. We got our headers, we're gonna add authorization. It's gonna be a bearer token.\u003C/p>\n\u003Cp>Raycast, great app as well, by the way. We'll paste in our API key and then the content type. Content type. Multipartformdata. K.\u003C/p>\n\u003Cp>Let's look for, like, a curl example. Document is path to your file. Okay. So it looks like we've got a document key and I'll pick up oh, the other downside of working locally. That might be the death of this particular project.\u003C/p>\n\u003Cp>Right? This is on my local computer. I've got 18 minutes. It's not going to be able to access that image. Let's do some more cheating.\u003C/p>\n\u003Cp>Right? Go back to our receipt images. I got the image address. Don't be mad at me for cheating on this. Alright.\u003C/p>\n\u003Cp>Let's take a look at that. We got a document. Here's the public URL of an image. Document can either be a file object URL or base 64. That should be savvy.\u003C/p>\n\u003Cp>Next time I'll have to do this on a standard cloud instance, not one of my local copies. Alright. So we got this. We're gonna take the webhook request. Let's see if they give us like a response what that response looks like.\u003C/p>\n\u003Cp>Date, confidence, document type, line items, locale, supplier address. I tell you what, let's just send this off and see what we come back with. Alright, so we're just going to pretend and actually this is gonna be correct because it's the same receipt but just a little cheating involved. We'll add a merchant name for this, We've sent this off to our auto magic receipt. It says unauthorized.\u003C/p>\n\u003Cp>Use token. What did I forget? Oh, token. Let's see. I've forgotten something.\u003C/p>\n\u003Cp>API reference authorization token. It's not bearer token. It's just token. Great. Let's go back and try her again.\u003C/p>\n\u003Cp>Keep editing. Don't forget to save your changes, kids. Alright. We'll create a new expense. Use the same receipt we've already uploaded so we don't duplicate it, and I really should remove that requirement.\u003C/p>\n\u003Cp>Go to our flows, See what we got. Bad request. Missing boundary and multipartform data. Alright. How are we gonna structure this, right?\u003C/p>\n\u003Cp>Missing boundary and multi part form data. Do we actually need the content type? Can we try that? See if we can get away with it. Otherwise, I think I'm gonna have to reformat everything.\u003C/p>\n\u003Cp>Alright. So we go to our flows. Let's take a look. Logs. Hey.\u003C/p>\n\u003Cp>There we go. Alright. So we've got our prediction. And we probably don't have to use form data because we're not passing an actual document. Cool.\u003C/p>\n\u003Cp>So we've got our document. This is a giant string, or a giant JSON object. Tons of stuff. Taxes, the value, supplier phone number, prediction, document type, expense receipt. Here's our line items.\u003C/p>\n\u003Cp>We just need our amount, right, and dates. So I'm just gonna copy this whole thing. Let's open up Versus Code. Do this and we'll look for date. Okay.\u003C/p>\n\u003Cp>So there we go. We've got our date. It'd be great if there was a way to copy that path. Right? Let's close, let's separate these, do this, do a little side by side action going with Versus Code.\u003C/p>\n\u003Cp>Alright, so now we need to update this item. How we doing on time? We've got 13 minutes. Let's get it done. So now we're gonna update our expense with the data coming from this API.\u003C/p>\n\u003Cp>And I may wanna do something like this where we say format data or I could just directly update that data. Anyway, so we'll go into update data, let's say update expense. We are going to use the permissions from the trigger, this is gonna be an expense. The IDs are gonna be coming from trigger dot body, dot keys dot 0, I believe. We'll take a guess and the payload that we want, we got the merchants.\u003C/p>\n\u003Cp>We wanna pass that. We want the expense date, I believe. The amount of the expense, and I think that would be a win. So let's look for the merchant, supplier, supplier address, supplier name. Alright.\u003C/p>\n\u003Cp>Let's start with the date. Right? So how do we get a hold of this guy? We've got our data. Alright.\u003C/p>\n\u003Cp>Let's just save this. That is our OCR receipt. That's our key. And of course because this is an invalid payload, it did not save. Alright.\u003C/p>\n\u003Cp>So our date expense date is going to be blah blah blah. We've got, OCR receipt dot data dot documents dot inference. I love that these guys are saying all this, dot pages dot prediction, debugging this is gonna be a nightmare, dot date dot value. That is deeply nested if I've ever seen it. Great.\u003C/p>\n\u003Cp>Alright. So next we've got the merchant. Let's try it. That is gonna be something probably similar, and I already see a typo. Prediction.\u003C/p>\n\u003Cp>Okay. So we'll just copy paste that. Alright. And that's gonna be called what? The supplier supplier name value prediction dot supplier underscore name dot value.\u003C/p>\n\u003Cp>Okay. Great. And then last but not least should be the amount. And we are closing in on 10 minutes on the clock, hopefully this thing works out. Would hate to start off on a loss.\u003C/p>\n\u003Cp>Alright. So the amount let's look for the amount. Total amount is what we're looking for. Point 99 confidence, that's great. That's what we're looking for.\u003C/p>\n\u003Cp>Got a lot of confidence in this. We got the total underscore amount dot value. Alright. So there we go. We've got our JSON.\u003C/p>\n\u003Cp>I'm just gonna copy it just to verify, update. Let's try it one more time. Let's delete these failed attempts. Alright. Tesco shopping receipt.\u003C/p>\n\u003Cp>We're gonna have to enter that in. Hit save and automatic receipt OCR. I don't have permission to access this because I don't have the proper key. So I didn't pass the key. I got the payload wrong as well.\u003C/p>\n\u003Cp>What did we get wrong here? Dot data, OCR receipt dot data dot document. Maybe it's just document. Maybe we can omit data. OCR receipt.\u003C/p>\n\u003Cp>And I also need to fix the keys because I didn't get those correct either. So it's actually just key. That's what's coming from the trigger, the expense. So body trigger dot body dot key. Alright.\u003C/p>\n\u003Cp>Fingers crossed. We'll run this again. This works this time. Let's hope so. Do our nice little t here.\u003C/p>\n\u003Cp>Boom boom boom. You don't have permission to access this. Still says undefined. So we're getting this back from the API. That's great.\u003C/p>\n\u003Cp>Trigger dot body dot key. The key should be correct, though. Payload trigger dot payload oh, no. This is an event hook. I'm sorry.\u003C/p>\n\u003Cp>So I've got it wrong. Should just be trigger dot key. No body. It's fine. Trigger dot key.\u003C/p>\n\u003Cp>That part's good. We are not good here, though. So let's do this. Let's disconnect this. Let's add a step.\u003C/p>\n\u003Cp>You just log this to the console. Log. And we are going to do get OCR receipt is what we're calling it. We're just gonna console log OCR receipt. Log OCR receipt.\u003C/p>\n\u003Cp>See what we're getting past. Alright. We'll drag that down here for now. We are running out of time for like a fun playing beat the clock. We'll hit save this.\u003C/p>\n\u003Cp>Let's go take a look. What do we got for him, Johnny? Log to the console, message dot data. Okay. So do we have to include message dot data?\u003C/p>\n\u003Cp>Worth a shot. Let's see. With 5 minutes and 40 seconds on the clock, Wasting precious time. So we have message dot data Message dot data dot documents. I'll just copy, paste.\u003C/p>\n\u003Cp>Alright. It's either gonna be a big finish or a sad ending. Not sure. We'll try it out and see. Alright.\u003C/p>\n\u003Cp>So we've got that same image, we're running it one more time. Flows do us nicely error update expenses set where invalid input syntax for type numeric. Still not getting the correct stuff coming from the app here. What in the world is going on? Alright.\u003C/p>\n\u003Cp>Let's bring in the big guns here. What are we what am I doing wrong? Logging to the console. Alright. Let's try picking up the data that we want.\u003C/p>\n\u003Cp>Let's go with, run scripts. Where are you? Format, data. Alright. So we're gonna get the data from receipt equals OCR data dot OCR receipt.\u003C/p>\n\u003Cp>Okay. And let's just return receipt for now. Let's string this together. Let's see if I could just get something going here. Format data.\u003C/p>\n\u003Cp>What's going on? It's gonna be a sprint to the finish. To to to log to the console. I can't even get my, so we'll drag that. Let's log into the console.\u003C/p>\n\u003Cp>Let's see what comes out of this. Expense, save, format data, flows, auto magic. Alright. So we got the run script, status. Okay.\u003C/p>\n\u003Cp>So we got data. We wanna pick up the data dot document. Right? I swear that's right. Data.ocrdoc equals receipt dot data dot document.\u003C/p>\n\u003Cp>Let's return the dock. We'll just keep drilling down. We got 2 minutes left. Let's see how this is gonna play out. Save t.\u003C/p>\n\u003Cp>Save it. Flows. If I was smart, I would have had multiple options. Okay. So we got this.\u003C/p>\n\u003Cp>Let's get down to our inferencing. Oh, I see what was wrong now. Duh. Pages is an array. That's where we were going wrong the entire time.\u003C/p>\n\u003Cp>Pages 0. Don't you love that? Message. It's not message. The message is coming from that console log.\u003C/p>\n\u003Cp>Alright. So with 1 minute and 21 seconds on the clock, let's save this and just see what comes up as the clock is ticking down. T. Now, if I hit save and stay it should be doing that for me, but it did not. However, did it?\u003C/p>\n\u003Cp>Did it actually do it? Expenses. There it is. Boom. With 49 seconds on the clock remaining, we have populated the receipt information from a third party API.\u003C/p>\n\u003Cp>Therefore, fulfilling our requirements for this, That is a wrap on the first edition of 100 Apps in 100 Hours. I feel like we need like a little Tiger Woods just, yeah, finish up strong. So if you enjoyed this series, or this episode of this series, stay tuned. We've got more coming like this. Would love for you to hop into our Directus Discord community and send us your feedback on what apps you would like to see us build next with our set of tools that we have here at Directus.\u003C/p>","Hi. Brian here. Welcome to the inaugural episode of 100 apps in 100 hours, where I will take some of your favorite apps and try to recreate them in under 1 hour or die trying. Hopefully not dying, but you get the idea. And first on our list today is Expensify. So we're going to build an Expensify clone. Expensify is expense management. And let's actually I'm not really sure I like the word clone here. Let's call this, a tribute, like the Tenacious d song. This is just a tribute because it would be very difficult to build all the functionality of Expensify in 1 hour. Obviously, they've built an entire business and they probably have thousands of developers, but let's get as close as we can. So when I think about Expensify, let's just go to their website, I think of the the first thing I think of is scanning the receipt, taking a picture of it, and it does the automagic stuff, where it automatically fills out expense reports for me, which, to be honest, is kind of a pain in the rear anyway. So, that is one thing that we're going to be concerned with, the let's just map this out. Automagic Receipt Management. We're going to have expense reports. Expense reports. So we wanna roll up all of those expenses, and, some of those are gonna be billable, And all those may be reimbursable, so we need to track for that. What else? We want to be able to approve expense reports. Seems like a good list for getting started. Alright, so I'm gonna pull up my handy tool of choice, Directus. So Directus, for those of you who aren't familiar with it, is a back end toolkit that allows you to build pretty much anything or everything, and get instant APIs on top of any SQL database. That's the pitch. I swear that's it. Let's actually run the clock, and I'm late on the clock here, but we'll start the clock 60 minutes on the timer. Let's go. We're gonna hop into Directus, and I am in my data model settings right now. And I'll zoom way in so you guys can actually see this alright. First thing we're gonna do is let's spec out our expenses. So let's just dive into our data model. We're gonna have expenses as one of our collections, so Directus is gonna create this table for us. And when we go to the optional system fields, we'll probably have a status. Let's go with the sort for the order on the expense report and we'll just select all these system fields because they have some presets that are useful to us. Alright, so we've got expenses, and I'm just gonna use a Expensify attribute. We'll create a folder here for this. We even give it like a nice green color. Boom. We're gonna organize all that inside our Expensify attribute. Okay. So the next thing, we are going to have an expense report. Right? We've got to roll up all those expenses somewhere. This is where we'll do it. Okay. Great. Okay. What else are we going to need? Let's just get started with this functionality. Alright. So, we go back to Expensify and let's try to figure out exactly what their data model looks like. My favorite way of doing that, short of interacting with the app is searching for their API. So we'll just go to the Expensify API, we'll go to their API reference, we'll just zoom way in on this, Expensify integration server, all you need to integrate. Alright. So request formats, we see what's going on here. Let's look for read and get policy lists. Maybe create. Okay. I see here's the expense creator. Alright. So, okay, it looks like here's a definition of the expense object. So reading through this, we've got a merchant, we've got the date of the expense, we've got the amount incense, we've got the currency, that's an integer, that's probably on purpose, three letter code of the expense, external ID, there's a category, there's a tag, is it billable, is it reimbursable, Comments, reports, IDs, tax. Okay. Straightforward enough, let's dive in. We'll go to our expenses and first, let's add our merchant. So we'll just create a new field inside our data model for Directus. This is just going to be a string type and clear enough, we are going to require value. We've got to have somebody that we got this receipt from. Let's change that to half width. And now let's move on to the amount. So let's do an input field. Now I could do this as an integer in cents. What I'm going to do instead is just use a decimal for this. So we'll do a decimal, I could change the scale to 2 points and, looks good. Alright. So now we've got the amount for the expense, we need to add a date for the expense. Expense date, let's just call that. So we'll choose the date time here and I'm gonna pick the time stamp as the type just because that will respect any time zones. I'm in the US. I use the 24 hour format. Or let's not use the 24 hour format since I'm in the US. No need to include the seconds. That's great. So we got our merchant. We got the amount. We got the expense date. Let's just do the name of the expense. Is that inside Expensify? Let's take a look. Created amounts, optional tag. Yeah. I'm just gonna give a name for the expense. Maybe we have some notes for that. Great. We'll add the text area field for that. We don't need any formatting. Great. Merchant amount, let's do a little cleanup on this. Directus makes it so easy to build apps that look great for your users. Alright. What else do we have? An expense comment. Got that. So then we have things like, is this billable? Is this reimbursable? So these are going to be toggles inside Directus. That's the name of the interface as we call it. Let's do is billable. I like to prefix those with is, just keeps things organized. Default value will be false, and looks good. Maybe we want to embellish this a little bit. So if you go into the advanced mode inside Directus, we can add notes for our users. Is this expense can we invoice this to a client? Great. Alright. Enabled. Let's just call it billable for the label. That seems a little better. The display is where it will show out show up in some of the different layouts inside Directus when you're looking at a list of records. Yeah. I don't see any need to change this. Great. Alright. We'll shrink that to half width. And I'm just gonna duplicate this one, and you can see how easy it is to build this out. Is reimbursable. Directus makes a a lot of this quick and painless. So we're gonna keep this on the expense collection. Is reimbursable. Yes. This is a reimbursable expense. We've got the user that created this, but, I wonder if there's a a use case where I have to expenses that somebody else submitted because they didn't do their job. So let's just add a owner, an expender. What is the the proper term for that? Buyer? Let's just call it an owner. Expense owner makes sense to me. That's what we'll call it for now. That is going to be our directus users collection. So that'll be the users of our app. Great. That's what we'll call it. That's the hardest thing in development is actually naming things. Okay. So this looks pretty good. This is a good start. What else are we missing for this? We've got a category, we've got the tags, Name of the tags. Yeah. We could we could do that. Let's let's build in a little more functionality. So we'll do tags. Maybe I wanna do tags and categories as a separate collection, and actually build those into the relationships. So category to me, there's only one single category that an expense can be in. Tags obviously could be many. So here we are going to use a many to 1. So this one, expense belongs to a single category. That's what we're gonna call it. Let's call it expense category. Great. And for our related collection, let's also do expense categories as the name of that. Now, I could click Save and Directus will do some magic for me, or I can open up advanced mode and go in and actually look at the details of this relationship. So if we look, we can see we've got this collection, we've got an expense category field, and Directus is telling me that, hey, we're gonna create a new collection called expense categories and we're gonna use the ID field of that to populate. Alright. The mini to 1, I could control the display template, but for now I don't have any other fields because that, collection is yet to be created. But we'll go in and go ahead and create this field here. And if I were to go back to my data model as well, we'll see that Directus created that collection for us. So now we've got our expense category. Let's go in and do tags as well. So tags are going to be a many to many relationship inside Directus. One tag could be applied to many different expenses, and many expenses could have many different tags. Great. You didn't know we were getting into data model expertise today, but let's name this thing. What are we gonna call it? We're just gonna call it tags. Right? We're going to create a related collection called expense tags and what else do we need to do? Show a link to the item so we can edit that tag as needed. Great. Save. Alright. So now we've got the basis of our expenses. Let's go in and flesh out these other ones that we just created. For expense categories, we're probably gonna have a name for that expense category. And, you know, maybe we got, like, an account number for our profit and loss, account name. Cool. Alright. And then for our tags, let's go in and add name and maybe we want to create a color for our tags. So I'm just gonna search for the color field here inside Directus. We'll give each tag a color. I could go through and preset these. If you leave this blank, Directus will actually create, give you a few of those presets that look nice anyway. Alright. So always gotta do my proper formatting. This is kind of a speed run, but one of the other things that I am a little OCD about, honestly, is cleaning things up and having a nice row of or nice columns of icons inside the app. So you can see a website project I've got working on here, but, you know, for now let's just roll with this. So we've got our tags, we've got our categories. The last thing that we need to do, if we open up one of our expense reports, which we have no data in that. We need to figure that one out. But we need a link back to our expense reports. So let's go into the data model. We'll go to expense reports. And this is really nice. We just recently added this search feature inside the data model, which is great, especially when you have hundreds of collections. So we'll go into our expense report. We're going to have a name. Do we even need a name? Yeah. We'll give it a name of the expense report. This could be a title. Bryant's second quarter expenses, blah blah blah, etcetera. Alright. Then we have a owner or could be like submitted by, you know, that's probably better but we stuck with owner on the other one so we'll just roll with that. For our related collection, we're gonna do direct us users. And you could pick these from a drop down over here on the right just by clicking those, but if you'll notice when I hit on one of the actual collections inside Directus, this turns purple just to let me know I'm doing the right thing here. So we got the owner of this expense report, yeah, I don't like that terminology, but I I don't wanna backtrack at this point. Date created. Alright. What else do we have? Then we're gonna need the actual expenses on this. So in this case, we've got a one to many relationship. So one expense report has many expenses. We're gonna call the key for this expenses, that's what it's gonna be stored as a column in the database as, and then we're going to pick our expenses as the related collection. And for the foreign key, we don't have one that exists already, but Directus will create one for us, which is nice. We're just gonna call it expense report because there is a single report it is linked to. And then I could go in and use the let's change this to a table view. We could see the merchant. Maybe we want to add the expense date, the amount, and, maybe the category. So here when it's a related collection I can just pick up the the related fields as well. Great, enable searching and filtering, hit save and it's probably good enough for now, right? Let's dive in and actually start creating some expense reports. So, I've got a new expense report. Brent's 4th quarter, it's not even the 4th quarter, 3rd quarter, let's just call it September expenses. Expenses. Great. Alright. So we go in, I'm gonna be the owner of this. I am not a user at this point, so I'll just use mister hop along. And now we can go in and add some of our expenses. Alright. So we've got Starbucks. The expense date is going to be this morning at, what, 7:30. I'm in West Virginia of all places, so we don't have a a ton of coffee options here. I know some people are gonna be mad that we've got Starbucks in here. That was expensive. So let's say 2102 has the amount. Coffee Needed that real bad. Alright. Is this billable? Probably not. Is this reimbursable? You know, we could have this where I could select this, but one of the other things you could do inside Directus is preset this to a default setting for, like, the the current user, for example. So for now, I'll just pre we'll pick hops along. We don't have any expense categories. I wanna say this would probably fall under meals and entertainment or just meals. No, you wanna divide those out. I hate doing bookkeeping, so accounting account number, I wanna say it's like the 3 or 4 100. Mills Entertainment. Great. Cool. Keeping track of all that. Tags. What are we gonna tag this as? Coffee would be the name of it. I don't really like any of these. Maybe we can pick, yeah let's just go with red for coffee. That's great. Cool. Alright. So we've got our information here. We've added our first expense. Pretty great. Cool. We've got our expense report. Yeah. The next thing maybe we want to fire off this expense report to somebody on our team. How are we doing on time? We're at 43 minutes, so that took about, 18, 20 minutes to flesh out this. How can we send this expense report to a team member, for approval? Well, first of all, let's do an expense report. Let's add an approved by. So we wanna track who this was approved by. That's gonna be a directus users, so it's gonna be one of our users. Great. And then maybe an approval date, Right. So we'll do a date timestamp, date approved. Great. No 24 hour format. So now we got 2 fields to track our approvals. Let's go in and use the flows inside Directus to send this report off or send a link to somebody to this report. So send expense report for approval. Great. And then we can even customize this. Let's do send. Yeah. There we go. Looks great. Scheduled send. We'll make this orange just so it stands out. Alright, so now I've got a list of different triggers. I want to actually trigger this one manually. So Directus gives us the option within a specific collection on the layout page or the detail page to send this off. So we will go in and look for expense reports. Okay. There's my expense report. Great. And then I could choose whether this is asynchronous. So, you know, do I wanna do this in the background? What are the locations where I can trigger this? So we wanna do on the collections and items pages. That's fine. We'll do both of those. If you are on the collection page, this will require a selection. We we do need you to pick this to actually send this off. And then next I'm gonna do this require confirmation because I get the ability to add different fields to the system, to prop for those fields when I send it. So I am going to add a new field for approver. So who do we need to send this? Approver. Who needs to approve this report. Great. Now the type I'm gonna pick here is gonna be JSON because we have a special little interface called the collection item drop down where I can pick a value from this and store it in JSON. So here again, we're gonna do directus users, and I'm gonna pick the user that I want to approve this report. And I could go in and as far as customizing the display template as well. You know, let's do the avatar, first name, last name. Great. And then maybe we even wanna add a note. Note. Close note to approver and that'll be just a simple string. No big deal. Great. Got the field width, let's make it full width. Actually, let's do a text area for that. Just a quick note. Great. Okay. So now that we've got that, what I'm gonna do, I'm gonna save this and I just wanna get a look at what it's gonna look like when I'm on my expense report. So over here on the right hand side, you can see Send Expense Report for Approval. I can select items or if I'm inside this expense report, I can click expense report, and then I get this confirmation dialogue of who needs to approve this, Matt. My note to Matt is going to be, hey, I need more budget for these videos. Great. Run flow on current item and let's just see what that came up with. So inside my flows, I can go in and check my logs and I could see what was actually sent down the pipe. So here's the trigger, here's the body of the payload, and this is actually what we received. This is what we will use to fire off that message. Alright. So this looks great. What I'm going to do, I'm just gonna copy this information, this is what I need, and I open up Versus Code. Let's start with a new window here. Where are you? New window. New file. Alright. So this is our trigger data. Great. Perfect. Okay. So now let's build the steps of our flow. We wanna send this expense report for approval, but we need an email to send it to, and we do not have that data here. So we need to actually get that data. So that's gonna be our next step. Get approval approver email is what we'll call it. And we will click read items from the database. The permissions from the trigger should be fine. I can also change this to full access if this is gonna be, triggered from somewhere other than inside the app. And for the collection we are going to use the directus users, but you can see this is not coming up in the search because it's a system collection. So I'm just gonna go in and hit edit raw value and we will do directus underscore users. That's our collection. Now for the IDs, we are going to use our little squarely bracket mustache syntax and we are gonna pick up the ID from this approver key that we selected. Alright, so, how do I do split screen here? Fancy fancy. Alright. So this is gonna be located at, so we use our brackets, mustache, we're gonna do dollar sign trigger. Trigger is the only one that has a special syntax. All the other keys of your operations get appended under, the key that you set here without the dollar sign. That's one of the big things that I see a lot of people get tripped up on. And it's gonna be dot body dot approver.key. Great. That's gonna be our ID. Do we need a query for this? I don't think so because we're picking up the key there. Let's hit save and then next we want to, I'm I'm just gonna save this. Let's run that one more time. So we'll go back to our expense reports. I'll pick Matt here. Hey, dude. We'll go back to our flow, and this is a good way of building flow so I can actually see the details of what we've got going on. Now I can see an issue right away. This for this demo anyway. This is gonna send to matt@example.com, which is not a real email address. So I'm just gonna quickly change this to one of my own emails just for demonstration purposes and to prove that we can actually build this app this quickly. Alright, we'll go back to our flows. Now let's actually send that email, and again when working with flows it's helpful to have like a code editor open, but, this case is fairly straightforward. We are going to send an email. We'll choose the send email operation and for the addresses we're gonna use that squirrely mustache syntax again. This is going to be get, this is where my memory struggles. This is called git approver email. So the 2 is gonna be git_approveremail, and that's gonna be the user object. So we're just gonna do email. I have to hit enter here to save this. We could also add additional emails if I wanted to if I wanted to cc myself. Let's call this expense reports. And the type here could be WYSIWYG. You can even choose a template if you are using our custom extensions, but let's go in do this as a WYSIWYG will be fine and we're going to do the trigger. So we we just want to pick up the content from our notes, which is gonna be body dot note. Should be Great. Alright. So this should populate that message into our WYSIWYG editor when we send this email out. Fingers crossed we're gonna save this. Let's test it out and see. How we doing on time? Alright. So we got about 32 minutes left, maybe a little less than that because I was, failing on throwing together the or I was failing to do the the countdown correctly. But, let's give this a shot. We'll go in, we'll send the expense report for approval, and we're going to send it to Rabat Miner. I need some more lattes. Alright. So we fire that off. Let's go back in and check our logs just to make sure. And it looks like the email went out. I'm just gonna open up my email inbox and voila, it it sent this out. That's great, that's all good, but, where's the actual expense report? There's no link. So let's go back in and what I'm gonna do, I'm just gonna capture the URL for this from my directus admin. Great. And then we're gonna go into our flow. We'll go back to that email and we just wanna give Matt like a button or a link or something here. So we're gonna do view and approve report. We'll enter in this email and we're gonna use our bracket mustache syntax and we'll do the trigger dot body dot keys 0. So the first item of the keys array is what we want to include. Cool. Great. Gravy. Save. Save. Alright. So now let's go back in. We'll send this expense report one last time, one more time with a link, hopefully. Fingers crossed, right? Alright, so now we wait and boom, we have a link. We click on the link, it logs us in and, Matt should be logging in to update the status on this. So we may want to go in and update the statuses for these. Let's do our expense reports. Let's change the status, draft. We'll do submitted for 1. Submitted. So submitted for approval. Approved. Maybe not approved is 1. Rejected. Let's call it rejected. That'll that'll be fun. That's a little harsh for an expense report, like, hey, your expense report was rejected. But, yeah, no worries. Oh, I goofed that up, didn't I? Should've saved before I started slamming away on the keyboard. Submitted. Alright. We'll just clean this up. Draft. Okay. Approved. Rejected. Alright. Let's just show the raw value for now or maybe a formatted value. That's fine. We'll do like a border, make it fancy looking, allow other values, no, allow no selection, no, for the interface though maybe it could be interesting if we do radio buttons. Great. Alright. Cool. So now we go in. Matt can approve this expense report and save it. But as you can see, I'm logged in as myself. I could potentially approve this expense report myself, which is is not cool. So we could use the roles and permissions inside Directus for that, where I could go in and say a given role like if we just had one called team member for example, I could go in and for our expense reports, I could go in and give custom permissions for this so that I cannot edit a certain field like the status. So this would prevent that user from actually, I'm sorry. I did that backwards. I would check all the other fields like the that user can update everything else about this expense report except the status or any other items that were that I needed. And likewise, one of the other things that I could do here is when I create a report, if we use the custom permissions for this, you could go in and define the presets where you can say, hey the current user is the default for this particular field. Sounds great. Alright. That is basic functionality of this. We've got the ability to add expenses, submit expense reports. You know, you might wanna try and tag into some other system to pay out these expenses, but for now I could stroke a check for those, I guess. But that leads us back to, boom boom boom, the automagic receipt management. So this part is a bit scary scary to me. I don't know how we're gonna do this in 26 minutes. Actually, I'm lying to you. I do know, what we're going to try because I've already cheated a little bit. Let's get that clock out of the way. I'm gonna make this full screen again. I have found this other service called Mindy Mindy? Mindy? Not sure what it's called, but it takes the receipts OCR, optical character recognition magic on them and apparently spits back out some data. Now, for the sake of time, all I've done is logged into this app and, set up an account. I haven't done anything else with it, so you're almost as fresh as I am at this point. I did see just a quick little tour tooltip that told me this is the API that I want. So let's take a look at at how we could do this. Anytime I upload an expense that has an image attached to it, and Directus is mobile friendly, so I could potentially log into this from my phone and update these expenses as well, just to to prove it to you there. Anytime I update one of those expenses, it has an image attached to it. We wanna send this off to this service and hopefully, I'd, like, not have to deal with actually entering in all the separate fields. Great. So first thing we need to do is, we got our expenses, let me close this one. We need to add an image field to upload the expense. So we'll go into our expenses. Let's add a image, receipt image or receipt file or we could just call it file. No need to get fancy with it. Alright. So now we've got the ability to upload a file. Let's start building a flow and then we'll incorporate this other service. So we've got, automagic COCR. Cool. Alright. So we'll do that on a vinthook. Let's do a action non blocking, so we don't wanna block the thread. Anytime a new item is created inside expenses, and we'll start there. Right? Alright. So let's go into, let's actually go back to our expenses. Let's just call this a new expense. Actually, we're not gonna do any of that. All I wanna do here and maybe we move this file up to the start actually. Let's move our file way up here. Alright. So the ideal scenario here is all I have to do create a new expense, upload an image of that, and then this service and Directus would do the rest for me. So let's just Google receipt images. Let's get a receipt image, see if we can find one that's pretty gnarly as well. What's this guy? Tesco Metro. Yeah. This one looks pretty beat up. This will be a good test, right? Save this image. Where did I go? Too many different tabs going on. So here's our Tesco shopping receipt. We hit save. Merchant value can't be null. That was a problem of mine, maybe I should go in and remove that, but you could set that up where it wasn't required. But now, if we take a look at our flow, that should have triggered our flow, we've got a payload, we can see here's the file that we're going to potentially pick up. Great. One of the other things that I wanna make sure, because we're gonna have to send this file to that service, is I'm gonna check our system collection and make sure that direct us files has public read permissions, and I could drill down to where, you know, only, files within a certain folder are available if I wanted to get very specific for this. Let's just leave it as is. Again, this is an Inexpanify Tribute, not a clone. Great. Alright, so now we've got our data coming from our new expense, we are going to wanna pass that to the mindd API. So let's see the API here. Alright. Load some stuff up for me. Alright. If I was smart I would have gone in here and cheated a little bit more than what I'm doing now. Alright. So I don't have an API key. How do I create an API key? API keys. Alright. We'll create an API key. We're gonna call this Directus. Great. Copy that API key. Don't you guys steal my API key? So So we'll go back to the API, go back to our documentation. Here's everything we're gonna need. Now is a good time to do the split screen stuff with, Arc. Great browser, still struggling, like 6 months on to figure this thing out. Alright, give myself a little more room. So we got the post request. We are going to use the send to, let's just call it OCR receipt. We are going to use the webhook request URL. We're gonna do a post request as it says. Here is our URL that we're gonna pick up. Hopefully, we could just copy that. Savvy. Of course it takes that post request. This looks okay. We got our headers, we're gonna add authorization. It's gonna be a bearer token. Raycast, great app as well, by the way. We'll paste in our API key and then the content type. Content type. Multipartformdata. K. Let's look for, like, a curl example. Document is path to your file. Okay. So it looks like we've got a document key and I'll pick up oh, the other downside of working locally. That might be the death of this particular project. Right? This is on my local computer. I've got 18 minutes. It's not going to be able to access that image. Let's do some more cheating. Right? Go back to our receipt images. I got the image address. Don't be mad at me for cheating on this. Alright. Let's take a look at that. We got a document. Here's the public URL of an image. Document can either be a file object URL or base 64. That should be savvy. Next time I'll have to do this on a standard cloud instance, not one of my local copies. Alright. So we got this. We're gonna take the webhook request. Let's see if they give us like a response what that response looks like. Date, confidence, document type, line items, locale, supplier address. I tell you what, let's just send this off and see what we come back with. Alright, so we're just going to pretend and actually this is gonna be correct because it's the same receipt but just a little cheating involved. We'll add a merchant name for this, We've sent this off to our auto magic receipt. It says unauthorized. Use token. What did I forget? Oh, token. Let's see. I've forgotten something. API reference authorization token. It's not bearer token. It's just token. Great. Let's go back and try her again. Keep editing. Don't forget to save your changes, kids. Alright. We'll create a new expense. Use the same receipt we've already uploaded so we don't duplicate it, and I really should remove that requirement. Go to our flows, See what we got. Bad request. Missing boundary and multipartform data. Alright. How are we gonna structure this, right? Missing boundary and multi part form data. Do we actually need the content type? Can we try that? See if we can get away with it. Otherwise, I think I'm gonna have to reformat everything. Alright. So we go to our flows. Let's take a look. Logs. Hey. There we go. Alright. So we've got our prediction. And we probably don't have to use form data because we're not passing an actual document. Cool. So we've got our document. This is a giant string, or a giant JSON object. Tons of stuff. Taxes, the value, supplier phone number, prediction, document type, expense receipt. Here's our line items. We just need our amount, right, and dates. So I'm just gonna copy this whole thing. Let's open up Versus Code. Do this and we'll look for date. Okay. So there we go. We've got our date. It'd be great if there was a way to copy that path. Right? Let's close, let's separate these, do this, do a little side by side action going with Versus Code. Alright, so now we need to update this item. How we doing on time? We've got 13 minutes. Let's get it done. So now we're gonna update our expense with the data coming from this API. And I may wanna do something like this where we say format data or I could just directly update that data. Anyway, so we'll go into update data, let's say update expense. We are going to use the permissions from the trigger, this is gonna be an expense. The IDs are gonna be coming from trigger dot body, dot keys dot 0, I believe. We'll take a guess and the payload that we want, we got the merchants. We wanna pass that. We want the expense date, I believe. The amount of the expense, and I think that would be a win. So let's look for the merchant, supplier, supplier address, supplier name. Alright. Let's start with the date. Right? So how do we get a hold of this guy? We've got our data. Alright. Let's just save this. That is our OCR receipt. That's our key. And of course because this is an invalid payload, it did not save. Alright. So our date expense date is going to be blah blah blah. We've got, OCR receipt dot data dot documents dot inference. I love that these guys are saying all this, dot pages dot prediction, debugging this is gonna be a nightmare, dot date dot value. That is deeply nested if I've ever seen it. Great. Alright. So next we've got the merchant. Let's try it. That is gonna be something probably similar, and I already see a typo. Prediction. Okay. So we'll just copy paste that. Alright. And that's gonna be called what? The supplier supplier name value prediction dot supplier underscore name dot value. Okay. Great. And then last but not least should be the amount. And we are closing in on 10 minutes on the clock, hopefully this thing works out. Would hate to start off on a loss. Alright. So the amount let's look for the amount. Total amount is what we're looking for. Point 99 confidence, that's great. That's what we're looking for. Got a lot of confidence in this. We got the total underscore amount dot value. Alright. So there we go. We've got our JSON. I'm just gonna copy it just to verify, update. Let's try it one more time. Let's delete these failed attempts. Alright. Tesco shopping receipt. We're gonna have to enter that in. Hit save and automatic receipt OCR. I don't have permission to access this because I don't have the proper key. So I didn't pass the key. I got the payload wrong as well. What did we get wrong here? Dot data, OCR receipt dot data dot document. Maybe it's just document. Maybe we can omit data. OCR receipt. And I also need to fix the keys because I didn't get those correct either. So it's actually just key. That's what's coming from the trigger, the expense. So body trigger dot body dot key. Alright. Fingers crossed. We'll run this again. This works this time. Let's hope so. Do our nice little t here. Boom boom boom. You don't have permission to access this. Still says undefined. So we're getting this back from the API. That's great. Trigger dot body dot key. The key should be correct, though. Payload trigger dot payload oh, no. This is an event hook. I'm sorry. So I've got it wrong. Should just be trigger dot key. No body. It's fine. Trigger dot key. That part's good. We are not good here, though. So let's do this. Let's disconnect this. Let's add a step. You just log this to the console. Log. And we are going to do get OCR receipt is what we're calling it. We're just gonna console log OCR receipt. Log OCR receipt. See what we're getting past. Alright. We'll drag that down here for now. We are running out of time for like a fun playing beat the clock. We'll hit save this. Let's go take a look. What do we got for him, Johnny? Log to the console, message dot data. Okay. So do we have to include message dot data? Worth a shot. Let's see. With 5 minutes and 40 seconds on the clock, Wasting precious time. So we have message dot data Message dot data dot documents. I'll just copy, paste. Alright. It's either gonna be a big finish or a sad ending. Not sure. We'll try it out and see. Alright. So we've got that same image, we're running it one more time. Flows do us nicely error update expenses set where invalid input syntax for type numeric. Still not getting the correct stuff coming from the app here. What in the world is going on? Alright. Let's bring in the big guns here. What are we what am I doing wrong? Logging to the console. Alright. Let's try picking up the data that we want. Let's go with, run scripts. Where are you? Format, data. Alright. So we're gonna get the data from receipt equals OCR data dot OCR receipt. Okay. And let's just return receipt for now. Let's string this together. Let's see if I could just get something going here. Format data. What's going on? It's gonna be a sprint to the finish. To to to log to the console. I can't even get my, so we'll drag that. Let's log into the console. Let's see what comes out of this. Expense, save, format data, flows, auto magic. Alright. So we got the run script, status. Okay. So we got data. We wanna pick up the data dot document. Right? I swear that's right. Data.ocrdoc equals receipt dot data dot document. Let's return the dock. We'll just keep drilling down. We got 2 minutes left. Let's see how this is gonna play out. Save t. Save it. Flows. If I was smart, I would have had multiple options. Okay. So we got this. Let's get down to our inferencing. Oh, I see what was wrong now. Duh. Pages is an array. That's where we were going wrong the entire time. Pages 0. Don't you love that? Message. It's not message. The message is coming from that console log. Alright. So with 1 minute and 21 seconds on the clock, let's save this and just see what comes up as the clock is ticking down. T. Now, if I hit save and stay it should be doing that for me, but it did not. However, did it? Did it actually do it? Expenses. There it is. Boom. With 49 seconds on the clock remaining, we have populated the receipt information from a third party API. Therefore, fulfilling our requirements for this, That is a wrap on the first edition of 100 Apps in 100 Hours. I feel like we need like a little Tiger Woods just, yeah, finish up strong. So if you enjoyed this series, or this episode of this series, stay tuned. We've got more coming like this. Would love for you to hop into our Directus Discord community and send us your feedback on what apps you would like to see us build next with our set of tools that we have here at Directus.","e1378f47-fa04-46d7-9c9d-ebba652412d3",[207],"fcb8a27c-5895-47f6-9644-13ea4c3b4aa3",[209,210,211],"3452bafa-9f28-43f2-9e14-3c5de754cb7e","039ef759-0fc3-4584-b7a4-a5205d3a21a8","992f81a1-0a4a-4fa8-8dee-10b4e3d0b848",{"id":172,"number":131,"show":122,"year":173,"episodes":213},[175,176,177,178,179,180,181,182,183,184,185],{"id":176,"slug":215,"vimeo_id":216,"description":217,"tile":218,"length":219,"resources":8,"people":220,"episode_number":158,"published":201,"title":222,"video_transcript_html":223,"video_transcript_text":224,"content":8,"seo":8,"status":130,"episode_people":225,"recommendations":227,"season":228},"lms","895940893","From creating and organizing course content, managing instructors and students, and tracking enrollments and completions, Bryant has sixty minutes on the clock.","73ceb5b3-6a79-484f-8254-a4d41ab970b3",64,[221],{"name":199,"url":200},"Mission: Learning Management System","\u003Cp>Speaker 0: Welcome back to the next episode of 100 apps, 100 hours, where we try to recreate and build your favorite apps in 1 hour or less, or die trying. Hopefully not dying. I'm your host Brian Gillespie, developer advocate at Directus. Super nice to have you. Today we've got a app that is near and dear to my heart.\u003C/p>\u003Cp>I've created a lot of courses in the past, so we are going to be building a learning management system, an LMS. There's a few examples that I took a look at in the the research for this prior to. I've got my proper dad attire on and we are going to dive in and build some apps. So let's just add a little bit of context for you guys before we start the clock. If you take a look at sites like Kajabe or Podia, these are online learning management systems.\u003C/p>\u003Cp>Most of these are software as a service where you can sign up for, $159 a month, it looks like, or $199 a month and sell your courses. The interface for these, if we can maybe navigate to one of, Podia's websites here. We've got like a main navigation on the left. We've got, you know, the courses that show up on the right. If we take a look at Udemy, that's a kind of a different case where you're on their platform, you're not white labeling, but you can create your own courses and load those up and sell those to their specific audience.\u003C/p>\u003Cp>So I think we'll land somewhere in between all of these different tools today. Should be very fun, very interesting. So with that, let's, let's dive into this. I'm gonna start my little mouse pose thing so I can highlight things on screen as we go through and build. So, I want to kind of show you what I am working with just so there is no, you're aware, there's no cheating behind the scenes here.\u003C/p>\u003Cp>I do have a Directus instance already started. I haven't logged in yet, but that is a blank Instance ready to go. That's what we'll use for the back end of the app. We also have a Nuxt 3 starter application. I do have like a Directus SDK already preconfigured and a couple of utilities just to make building a little bit easier.\u003C/p>\u003Cp>But aside from that we can see this is just a blank slate. There's a login form, text component and an upload component, just to make this a little easier. A lot of this is boilerplate functionality, I don't want to bore you guys with that. So that's the setup. Alright.\u003C/p>\u003Cp>Let's dive into building the app and we will start the clock on this. So we now have 60 minutes to develop this out, But before we actually do any code, I want to like sketch this out. And I I love using Figma for this. So the first thing we want to flesh out is our data model. What's our schema?\u003C/p>\u003Cp>We're going to have courses. So within each course, we have some lessons. We probably have some modules. Alright. Maybe these are not huge per se.\u003C/p>\u003Cp>We'll just make them smaller. What else are we gonna have? We're gonna have users. We'll probably have some instructors. Right?\u003C/p>\u003Cp>We want to be able to track the instructors on the course. This is probably a a pretty good starting point. Now as far as the functionality that we need, so let's just call this our data model. Cool. We'll go over here.\u003C/p>\u003Cp>Inspect this out. Functionality. Alright. Let's go through and flesh this out a little bit. We want to view a list of courses, view an individual course, and this is gonna be on the front end.\u003C/p>\u003Cp>View individual lessons, we probably need to sign up for our course. We just walk it through the entire process. View individual lessons. We want some authentication, so we wanna be able to log in. And, what else?\u003C/p>\u003Cp>We wanna be able to track progress. Right? And, to that end we probably need like a collection to track enrollments. So who are our users, what courses are they enrolled in, what is the completion percentage for those. Alright.\u003C/p>\u003Cp>So this seems like a pretty good start. We'll continually come back and refer to this and refine it as we go through. But the first thing that I wanna do is start into our back end. So I'm gonna pull up Directus in this case, and let's see if we can zoom in just a little bit to make it easier for everybody. Now I've got this running on Docker locally, but the easiest way to run Directus is using Directus Cloud.\u003C/p>\u003Cp>And I need to make sure I've got the correct password here. Alright. So we've got a blank Directus instance. We're going to flesh out this data model in no time at all. So let's create our 1st collection, we'll call it Courses, and we will use the generated UUID as the primary key field.\u003C/p>\u003Cp>We'll just go in and for now we'll just select all of these defaults for tracking date created, user created, etcetera, handy to have. And for our courses we probably want a name for the course, that's great. We want to have a description for the course, so that will be a WYSIWYG editor. That way we could support HTML and rich text formatting. What else do we need as as far as our course data?\u003C/p>\u003Cp>We've probably got an image for the course as well. So we could call this image. We could have multiple images, but let's just stick with a single featured image, and we'll use that. Great. So now if we back up, we've got the start to our courses data model.\u003C/p>\u003Cp>Let's go in and flesh out the other parts of our course. So each course will be made up of several different modules. So the modules, again, we can add all these fields. We may not need those. I typically just add these because it has some preset functionality.\u003C/p>\u003Cp>Obviously the user created it will store the UUID for that particular user, updated date and times, those are all handy to have. So the modules, we will have a name for the module, and, you know, we could have like a short description for the module. Let's just stick with name, we'll keep it really simple. The other thing that we may want to have on the courses, this just reminds me, is maybe something like a slug. So just a pretty URL for that specific course.\u003C/p>\u003Cp>So we'll go in and set that up. One of the other things I could do really quickly in Directus is make sure this is URL safe. So there's just a, a field to set that up for us to automatically format that. Next, let's go in and add our lessons table, or lessons collection. We'll do the same, so what's the status, is this published or is it draft?\u003C/p>\u003Cp>And now in the lessons we'll have a name for the lesson. A lot of names here. We'll have the, let's call it Lesson Content, and that could be rich text. And because these are primarily videos, let's do a video URL. We can host those on Vimeo, YouTube, Bloom, however you wanted to do that.\u003C/p>\u003Cp>You could also just actually upload these into Directus as well. Alright, so now we've got kind of this side of the equation figured out. Directus already gives us users out of the box, which is great. We can go in and add these other collections. We'll have an instructor's collection.\u003C/p>\u003Cp>Instructor's, I hope that's how you spell it. So our course instructors, that will be we'll have a name for them. Let's have a bio, so that'll be a WYSIWYG field. And one of the things that I love most about Directus is just how easy it is to map out this data model and get a complete back end with an AI or API ready to query for my front end. So instead of working with dummy data, I can actually prototype live with real data.\u003C/p>\u003Cp>So we got our name, we got our bio, let's do an image, or we could call it avatar. Great. Cool. And last but not least, we've got enrollments. So we'll just, create all these separate tables, all these different collections within Directus.\u003C/p>\u003Cp>And now we can go back and flesh out the relationships between these, because that's where the tricky part comes in. Or not necessarily tricky with Directus, but, we wanna make sure that data model works correctly on the front end. So the when we start mapping these out we have our courses. Each course could have multiple modules, and each module could have multiple lessons. So those are one too many relationships inside Directus.\u003C/p>\u003Cp>So if we start fleshing this out, we'll go into the Courses collection and we'll use the relational fields inside Directus. So behind the scenes Directus will set up these relationships inside your SQL database and make sure everything plays nice with each other. So we're going to call this modules. So that's going to be our key. The related collection here is going to be modules.\u003C/p>\u003Cp>And then the foreign key, so our key inside the modules collection is going to be Course. We could choose whether we want a list or table layout, that's fine, and we'll just use the name of the module and we'll show a link to that module. So now our modules are set up and if I were to go to the actual module section we can see we've got the reverse relationship back to the course. And maybe we drag and drop this, give these some icons to make them look nice. So we've got a Course, maybe that is a folder looking object.\u003C/p>\u003Cp>It's great. Let's use black as the color. So we'll customize this a bit and make it look nice. The module, you know, let's look for a list, maybe. Looks great.\u003C/p>\u003Cp>And now let's add a lesson, we'll just use like a video icon for that. Perfect. Okay. So now if we go back to our modules, again we're going to create a one to many relationship, but this time we're going to do that on our lessons. So the key here will be called lessons and the related collection will be lessons.\u003C/p>\u003Cp>Our foreign key will be module. So just the singular because each lesson can only be in a single module. And we'll use the name of that lesson. Great. We'll show a link to the item.\u003C/p>\u003Cp>And now we've got that structure set up. Great. How do the other items relate? Right? We've got instructors.\u003C/p>\u003Cp>Instructors could have many different courses inside our platform, and a course could have multiple instructors. So that's a good use case for the many to many relationships inside Directus. And what we'll do is go to either courses or instructors, doesn't really matter which one. And instead of a one to many or a many to 1, we're going to use the many to many relationship. So here we'll call this instructors, that's gonna be our key.\u003C/p>\u003Cp>For our related collection, we're gonna use instructors. And we don't want to allow duplicates, we want to show a link to the item. If I wanted more control over what Directus creates, or uses for the name of the junction tables, I can go into the advanced field mode. So we can see in our junction collection Directus is going to create this inside our SQL database. Courses underscore instructors, and there's gonna be a Courses ID and an Instructor's ID.\u003C/p>\u003Cp>I can also add this relationship to the instructors field and we'll give this a sort just in case we want to have a head instructor and, you know, prioritize who is the primary instructor on the course. So we'll hit save, Directus will do some magic behind the scenes, and if I go in now I will see a hidden collection in our data model for courses underscore instructors. Savvy? Everything's looking good so far? Great.\u003C/p>\u003Cp>Alright, now we also want to flesh out our enrollments. So if we think of enrollments, enrollments are going to be the users that are enrolled in the individual courses and this setup will be a little bit different in that we go to Enrollments, there's gonna be a many to one relationship. So each enrollment can only have one course. So what are the course that this person enrolled in? Great.\u003C/p>\u003Cp>And then we're also going to have a many to one relationship with the Directus user. In this case we could just call it user as well. But the related collection here is going to be the Directus underscore users. That's our system collection for users within directus that, gives us all the authentication and user access and permissions. We could also keep track of progress or lessons completed, but for now let's just roll with this.\u003C/p>\u003Cp>Okay, a little OCD over things like icons and colors, so I will add some icons and colors here. We've got instructors. These will be people. Let's see what we've got. Nice friendly guy waving, that's perfect.\u003C/p>\u003Cp>Let's make that purple as well. Alright, so now if we look at our data model, this is looking pretty good. We will go into the front end and we can see that we've got courses, we've got modules, we've got lessons. Let's just create a course. Let's do published.\u003C/p>\u003Cp>And we'll call this 100 Apps in 100 hours, great. Watch to follow along as we build an LMS. While building an LMS? I don't know. It's kinda meta.\u003C/p>\u003Cp>No worries. Alright so we can, let's go to Unsplash and find a nice looking image. One of the nice things about Directus, and you'll hear me say that multiple times throughout this series, is the ability to import from a URL. So I can just copy that URL there, import that image, it will store it in my file library for me. And we'll give this a slug.\u003C/p>\u003Cp>So you'll see if I press space bar it will automatically format this for me so that it becomes URL safe. And let's go in and create a module. So we'll call this 1st module. We'll create a second module. Great.\u003C/p>\u003Cp>And we'll add an instructor. The dude building stuff and videos. Cool. I've got let's upload a photo. Great.\u003C/p>\u003Cp>Good. Solid. Okay. So now we've got our first course. Since we've set up these collections I could start querying this on the front end using the Directus APIs.\u003C/p>\u003Cp>So So if I just open this up in a new tab, I'm going to change the URL a little bit. We'll go to items/courses and boom, we get this permission access error. So I haven't enabled permissions. We've created this actual, collection. We've created several of these collections, but for the general public all these collections are access is forbidden by default.\u003C/p>\u003Cp>So I could open this up and you know, just allow public access for now to all of these. Once we went to production, obviously we want to restrict this per user. But if I just refresh you can see over here on the left, excuse me, we've got our data coming from the LMS, or the back end of our LMS, our direct us instance. Great. So we are about 15 minutes in.\u003C/p>\u003Cp>We've got our first bit of data modeling done. Let's dive into kind of the front end and start building, right? We will, I'm just gonna move this out of the way for now, and we'll get to a blank slate inside our code editor. We've got our direct assistance up here, but let's pull this up. Now within the pages directory of my Nuxt application, maybe we create a new folder for courses.\u003C/p>\u003Cp>This will give us a route slash courses on the front end. And we'll have an index route. That's great. We'll set up a default view component here and now we can start querying that data from Directus. One of the things that I do have in my little Directus module is a composable to actually query the front end of or query the Directus back end.\u003C/p>\u003Cp>So we can do something like this where we say const courses equals useDirectus. And this is using the Directus SDK on the the back end. We're gonna read items, courses, And I could go in and add some options here as well. You know, Copilot is showing me like a filter for published. But for now, because we haven't published anything, let's just take a look at this.\u003C/p>\u003Cp>We'll, log this out to the front end. And if we navigate to /courses, it looks like we have an object promise, so we wanna do await use directus, and boom, now we've got our data on the front end. Cool. So now we probably want to do a little bit of formatting, you know, add this, add maybe a card for this. One of the things that I am using or I've got set up in this Nuxt instance or this Nuxt template app is the Nuxt UI library, which is just a UI library put out by the Nuxt team.\u003C/p>\u003Cp>It helps you build apps that look great faster. It works perfectly for stuff like this. On a lot of other projects, I may use, you know, my own custom coded components. But for now let's do a card. We'll do v4 courses and courses because that is an array, course ID.\u003C/p>\u003Cp>Let's actually just do something like this where we've got v text. I do also have a text component, we'll call it course. Name. And we can use the Nuxt image component, course. Image.\u003C/p>\u003Cp>See how far that gets us. Okay. So we've got a card. We probably want to give a little bit of styling to the container. What is the I guess the muxu container It constrains the width.\u003C/p>\u003Cp>Okay. And then maybe we've got let's just go back. We'll do the keep this really simple. I also, use tailwind a lot. We'll do like a max width of 4 XL for now.\u003C/p>\u003Cp>Make this in x auto. And maybe we set these up in a grid with 3 columns on, like a large size. Okay. So we've got this actual course card. We're not rendering the actual image, which, is concerning me.\u003C/p>\u003Cp>You don't have permission to access this. So we've hit our first kind of snag here. Somehow I've managed to log myself out of Directus as well. So let's log back in. Directus.\u003C/p>\u003Cp>Alright. And now we will go in and we forgot to set permissions for our system collection. So all the Directus files, we're just going to make those public for now so we can take a look at that file. I could now see that and it should show up within our card as well. Great.\u003C/p>\u003Cp>You know, maybe we make this font bold, 2 x l. Make it a bit bigger. Let's make it giant, right, 4 x l. I need to take a look at my Vtex component actually. I think I've already got sizes on here, so we could just use the size prop instead.\u003C/p>\u003Cp>2XL. Great. Okay. Alright. So now we've got kind of an index page.\u003C/p>\u003Cp>This is a course listing. Let's wrap this with, another div. I'll go in and paste that. So now we've got a course listing, we've got a course card, How do we get to this actual course detail? So I will go in and, now we'll go into our courses folder within the pages directory And let's create actually let's create a new folder.\u003C/p>\u003Cp>And we'll use the brackets for this. This will give us a dynamic route. And we will use like slug or I could call this course or ID. We're using slug as the property for the course on the directive side, if I remember correctly. So we've got a slug for each of the courses.\u003C/p>\u003Cp>That's what we'll query by. Sounds great. And we will go in and now we will add like an index page for this course. So again, we'll build a detailed page here, just a simple index page, and let's do course, wait, use direct us, read item. We wanna read a single item.\u003C/p>\u003Cp>This is using our SDK context. And we'll use route params slug. That's not gonna cut it. Actually, we want to read multiple items. And the reason why is because we're giving each course an ID, a a UUID.\u003C/p>\u003Cp>And the Directus SDK, when you read a single item, you can look up by that primary key, in this case the ID field. But where we want to look up by the slug, we're going to use read items and then we're going to construct a filter. So it'll be like this where slug is equal to route dot params.slug. We're gonna use route. So we'll pick up the route from view.\u003C/p>\u003Cp>We use route dot params.slug. And again, this is why I love building this way with the back end first because I can use actual data to flesh this out. So now on our course listing page we want to add a link to that. So if we go back to the index page, we can wrap, both of these in a Nuxt link component. And for the to prop, let's just add, courses slash course ID or no, course dot slug.\u003C/p>\u003Cp>Alright, so now we should get a clickable link. We can see at the bottom of the screen it says 100 Apps, 100 Hours. And when I click on it it will give me, an array of the data in this case. But I would rather have just a, I'd rather have the actual image. So let's do something like this where constant course equals courses dot I or core the first item of the array.\u003C/p>\u003Cp>Maybe this could be a computer prop as well just in case that changes. Courses dot value cannot read properties of undefined. What is going on here? Let's back up and let's just keep it simple. Right.\u003C/p>\u003Cp>Course equals courses. Okay, there we go. So we've got our course, and now we can start fleshing this out. So if I go in and I look at the Nuxt UI Library, we've got some components here, we've got like a skeleton. We've got a container.\u003C/p>\u003Cp>We've got a card. What do some of these other apps look like? How do we actually get to their courses? Not a lot of great examples on the website. If we look at Udemy, we could see we've got like a course name, we've got a preview of the course, we've got, some of the lessons here.\u003C/p>\u003Cp>And then when you get into the actual, enrollment of that specific course, they've got like a kind of a sidebar layout. So let's create a new section. We'll do the course title, course description, follow along as we build. Okay. 100 apps, 100 hours.\u003C/p>\u003Cp>So the course description, we're gonna use v html for that. Course dot description. Okay. And now we've got that. We could apply some nice styling for this.\u003C/p>\u003Cp>Let's do the font. Black text 4 x l. We'll do a, let's try that U container again. And this is coming from the Nuxt UI library. Alright.\u003C/p>\u003Cp>So we got that. We've got, the description. Maybe we wanna add like a small bit of text to label that. This description class text extra small, text gray 500, something like that. And we can space these out a bit.\u003C/p>\u003Cp>So again, this is a bit crude. We're going for speed over beauty in this case. But we want to flesh out this functionality. Where did you go? Okay, so we got our course name, we got a description.\u003C/p>\u003Cp>Let's show our instructors. Alright. So this is gonna be our pclasstext. Let's do instructors. Okay.\u003C/p>\u003Cp>So we've got that. We'll add a bit of margin to it. Space it apart. We'll pull up the instructors. How are we doing on time?\u003C/p>\u003Cp>Quick time check. Got 30 minutes remaining to flesh this out. Gonna be a definite challenge here. So we've got the instructors. We'll do, should be course dot instructors, right?\u003C/p>\u003Cp>Let's just log that out and see what we get. Course dot instructors. So you can see Directus is giving me back an array of the different ID's for those instructors. I've said this multiple times, but this is one of the reasons why I really love Directus. Because I can go in and for, this is using the REST API.\u003C/p>\u003Cp>I can actually go in and do something like this where I say, hey, I want all the root level fields. And then for instructors, I want, I want a list of all the root level fields for the instructor. So I could get all the related fields in a single API call. Now where this is a many to many relationship, you can see I need to drill in one more level. So I have to go in and do one more level where I have instructors underscore ID and grab all those fields.\u003C/p>\u003Cp>So now I can see the instructor and we can set up like a, just a v four loop here. V four instructor in course dot instructors. And it looks like Copilot has added a lot of code that I did not mean to add here. So let's just pull that back. Be careful how many times you tab, right?\u003C/p>\u003Cp>So now let's take a look at instructor within that loop and see what we have. Are we even getting anything? Alright. We've got instructors. We've got the instructor ID.\u003C/p>\u003Cp>K. So we could do something like this. We could actually destructure that. Or can we? Instructor instructor's ID.\u003C/p>\u003Cp>And let's see what we get back with that. We're breaking stuff. Okay. Yeah. So we'll just run with this for now.\u003C/p>\u003Cp>Alright. So we've got our instructor, we've got an instructor ID. So we'll have instructor dot instructorsid.name. Let's wrap that P TECH. We'll do, Nuxt Image.\u003C/p>\u003Cp>And let's do, yeah. Maybe width 24 sounds good. 24 height, rounded full to make this a circle. And then the source is gonna be instructor dot instructorsid.avatar. And then we can do a divvhtml for the bio equals instructor dot instructorsid.bio.\u003C/p>\u003Cp>Okay. Cool. So now we've got a few things going on here. Let's actually look at the lessons. Right?\u003C/p>\u003Cp>We'll go into our Directus instance. If we go back to our specific course here, we didn't add any lessons to this specific module. So I'm just gonna pull up the Directus YouTube account. Our YouTube studio here will get to the Directus channel and take a look at our content. So I'm just gonna copy a few of these down.\u003C/p>\u003Cp>We're gonna add a couple of lessons within each one of these. Just gonna actually take these wholesale from the YouTube channel. Here's some content. Great. Alright.\u003C/p>\u003Cp>How do we do, we'll copy some of this. How to manage different versions. Blah blah blah, copy that video URL. Oh, that's actually a short. Not sure what that'll do.\u003C/p>\u003Cp>Let's, let's take an actual video. Great. So we've got our first module. Let's go into the second module and we will add one of my videos. Getting started with Agency OS.\u003C/p>\u003Cp>My wife tends to make fun of, my sausage fingers all the time. So lots of typos here. So we'll go in. Let's just copy one more of these so we've got something to look at on the front end. Project templates.\u003C/p>\u003Cp>Great. Okay. So So save this. Now we've got our course, we go back to our course detail page in this instance and, you know, let's add a new section over here where we've got our section. Looks like it get hub copilot for the win.\u003C/p>\u003Cp>We've got our lessons. Give that some margin. Give some space. And, so if we log each lesson, let's just take a look, we'll probably get the same, let's just close this out real quick. We'll do course dot lessons just to see what we're getting back from the API, which it doesn't look like anything, right?\u003C/p>\u003Cp>So if I open up the dev tools, we take a look at the network request, we dive into the data, the lessons are actually going to come through the different modules. Alright, so again, we can go back up to our fields section and we could do something like this where we go to modules, then we, drill down again into, our lessons. And we want to grab the module name. And within our lessons, again, we could just grab all the root level fields within that. And now we can see that second API call that was made.\u003C/p>\u003Cp>We've got our modules. We've got our lessons within that. And again, I only have to make a single API call. You know, we can certainly cache this if this data doesn't change, but it keeps the entire app very snappy and prevents me from having to make 35 different calls to get all the related data. Also kind of it follows a very GraphQL like structure as well where I can request just the data that I need.\u003C/p>\u003Cp>I don't necessarily have to go in and use these wildcards to grab those root level fields. I could go in and say something like name, description, that way I can prevent over fetching and larger network requests than I actually need. So we've got, now we've got our data coming in correctly. Let's go through and we're going to loop through the modules. So we'll do v 4 module in course dot modules, and then inside that we'll have another loop for the lessons.\u003C/p>\u003Cp>So we've got our second module. Let's style these a bit. So maybe we wrap this, do something like divide y and divide y, divide gray 300 maybe? Okay. So now we've got a divider between our different modules.\u003C/p>\u003Cp>And maybe we give each module some padding. That's probably a bit so we'll do p y 4. Let's style the module a bit And font, let's just give it a little fancier font. I think I've got this set up as maybe Poppins inside my specific account. Font display.\u003C/p>\u003Cp>Great. Div 4. Okay. We got the we don't really need the lesson content. And then let's style each one of the lessons.\u003C/p>\u003Cp>You know, we'll likely change these to a NUCs link and then the 2. Now we want to dive into each individual lesson. So our URL structure is going to be courses, we've got course, so we use the dollar sign, so so we get the template literals here. We've got course dot ID and then we've got lessons and then maybe lesson dot ID or it we could use slugs on the lessons as well if we wanted to. Let me fix this.\u003C/p>\u003Cp>We've got slugdot ID. Okay. Unterminated template literal. Okay. Alright.\u003C/p>\u003Cp>So now we've got a URL that should take us to this specific lesson. I'm gonna go back into our Directus instance, which keeps logging me out, probably a cookie or some type of setup issue on my end when I configured Docker, but we'll work around it for now. Let's go in and give a slug for the individual lessons as well. So, just to maintain parity. We'll go into the interface, we'll make slugify checked, and now we've got a slug for the lessons.\u003C/p>\u003Cp>Let's go into each one of those lessons and set a slug. Project templates, managing versions. Alright. Getting started. And custom operations.\u003C/p>\u003Cp>Okay. So now each one of those have a slug. We can change this from ID to slug. We'll still use that ID as the the key for looping over these. But now we're starting to get something that kind of looks like, some courses.\u003C/p>\u003Cp>You know, maybe we wrap these in a go back. Maybe we wrap these in a card component. And a lot of times as I go along, I would definitely be refactoring these as we went along into components that made sense. So we've got the padding, p y 4. Let's do some space between each card.\u003C/p>\u003Cp>So we've got a little bit of space between each card. That's great. There's our different modules. I don't know why the first module is showing, below the second module. That could be the way that we've got our sorting on the course.\u003C/p>\u003Cp>So if we go in, we don't have sorting enabled. So what I can do is go into our Courses section, our Courses Data Model, we'll go to the relationship field, or the relationship tab, and for our sort field we're just going to add that sort property, or that sort field on that. And that should take care of the sorting for us. So now we've got our first module, that's great. We've got our second module, starting to look like something.\u003C/p>\u003Cp>Let's just take one moment and make a, like a nice little header for this. We've got image course dot image, object cover, And we'll give this a little bit of padding as well. MT 8. Maybe we want to round the corners. Again, very crude but, definitely paints the picture.\u003C/p>\u003Cp>So we've got about 18 minutes left. Let's dive into the actual lessons, Right? So a pretty common set up when we go to the lessons, and right now we're getting a Page Error Not Found, is to have a list of the other lessons over here on the left hand side, and then on the right hand side we've got the actual course content. So let's figure out what that would actually look like inside here. So one of the nice things about Nuxt is like the nested routes.\u003C/p>\u003Cp>So we go to their documentation, we go to, it's like child routes. Child route keys, nested routes. So I can set up, like parent and child collections, or pages, within these nested routes so that, it's a nice way to do routing, where I can have a child page show up in the parent view. So let's go in and we've got our slug here. So this is our actual course.\u003C/p>\u003Cp>Within the course, maybe we want to show let's do a slug. I don't know if this will be it or not, slug dot view, So we'll create a new folder for lessons and then we're going to create a dynamic route for the lesson. So we'll call that lesson dot view v comp ts. We're going to use the route, so route equals use route. And here I'm just gonna log the params to make sure we're on the correct spot.\u003C/p>\u003Cp>Alright. So we can't find the slug dot view. That's great. Let's add a component for that. Courses, 100 apps, lessons, managing versions, slug.\u003C/p>\u003Cp>Let's just say this is the course parent. What does that get us? Alright. So we've got managing versions. We don't see the course parent part of it here.\u003C/p>\u003Cp>Okay. So let's call this lessons. And now all we should see is course parents. But if I go in, and I can't remember the exact syntax, so let's go back to nuxt.com. We'll do the child routes, nested routes.\u003C/p>\u003Cp>It is what? NUXT page. So we just throw this NUXT page in here And now we see this where we have Course Parent, and then we have our NUCs page which gets rendered inside that. So why are we doing it this way? Because we can actually fetch the lessons within this parent component and then, that will not re render, but then we can navigate within these individual lessons and render those over here on the right.\u003C/p>\u003Cp>So if we do something like this where, let's flesh this out, we've got a, let's do a div. Alright. We could do, like, an aside maybe. This is the list of lessons. Then we've got another div.\u003C/p>\u003Cp>We'll render that, and we'll just flex these. Flex. We'll add, let's just maybe give this a fixed width of like 56 or 64 wide. Alright. And just to illustrate this, we'll give it like a background, a bggray100.\u003C/p>\u003Cp>So we've got our list of lessons there. You Gonna add some padding for that, p 4. Then our next page, we probably got a new container. I need to actually look up order the the settings for that. Let's take a look at it.\u003C/p>\u003Cp>You container, constrain the width of your content. Max width 7 x l. Okay. Yeah. Works fine for me.\u003C/p>\u003Cp>Alright. And this could be probably height full. We want it to be the full height or height, screen. That'll get us. Cool.\u003C/p>\u003Cp>Alright. So now we can go in and render the list of lessons here. So we will grab our lessons. So let's do const lessons equals await. You got GitHub Copilot for the win.\u003C/p>\u003Cp>We're gonna read the items from the lessons. The filter is gonna be where the course dot slug equals route dot params.slug. So we're gonna pick up the we can pick up the course from the course slug. And let's actually see what we get here. I refresh the page.\u003C/p>\u003Cp>We're breaking some stuff. Course, slug, route. Oh, we actually have to grab the route, don't we? Forbidden. So I can't get this information.\u003C/p>\u003Cp>Wonder why that is. Let's take a look. We'll just erase that filter, see what data we're getting back, in this list of lessons. So I'm gonna quickly wrap that and, just output the lessons. Okay.\u003C/p>\u003Cp>So there's our individual lessons. Looks like those are actually coming through. Okay. But if we go to course dot slug is equal to, Let's just take a look at our network request. Alright?\u003C/p>\u003Cp>This is being fetched on the server side. You don't have a permission to access this, which makes me think something is wrong. Alright. So let's just look through this really quickly to see if we can find our module. Duh.\u003C/p>\u003Cp>That's because our modules are not, our lessons are not actually linked to our course, they are with our modules. So the the courses belong or the lessons belong to the individual modules. So, fun debugging issue here, but let's just go into our original index where we get the course, we get our lessons, and we can actually copy this code. And within the lesson route, her lessons where's our parent component? We could do this.\u003C/p>\u003Cp>Right. And here we could probably adjust it where the slug or the course, slug. So we get all the modules instead of the lessons, which is a little bit counterintuitive. But we still wanna display all those modules within that bar anyway. Alright.\u003C/p>\u003Cp>So we get our modules. We'll change this to modules. And we'll make sure that we grab the lessons as well. Let's just clean this up and refresh. Something is breaking.\u003C/p>\u003Cp>We don't get our modules. Route is not defined. Again, silly me, silly rabbit, you have to use the route. Alright. So now we can loop through these modules.\u003C/p>\u003Cp>And what does GitHub Copilot got for us? Alright. So we've got our different modules here, how to manage different versions, how operations can be completed. Okay, great. Nuxt link courses, route planarams.\u003C/p>\u003Cp>Slug. And now if I am navigating between these over here on the right you can see that this is actually changing. You can see that we're logging the slug for that specific lesson. So if we go into the lesson, we've got route params. Lesson.\u003C/p>\u003Cp>Now within this specific component we will call the lesson that we want. But what we'll do is make sure that we update this a little bit. And we're gonna use the lesson equals lessons if we just log lesson over here. Alright. Boom.\u003C/p>\u003Cp>We can see that. If I go back, I wanted to make sure this doesn't shrink. Select shrink. 0. Alright.\u003C/p>\u003Cp>And now on our individual lesson, let's actually set this up. So we got the lesson name. We got the lesson content. It's great. And then we will have a where it'll be an iframe?\u003C/p>\u003Cp>For the YouTube Embed. And I actually think do I have do I have Do I have a video string for this? I don't. Generate lesson and video. What is the YouTube embed?\u003C/p>\u003Cp>Alright. So we're getting the URL from YouTube. So if I log in to the lessons here, how do we transform this? Closing in on what? We're gonna cut this one down to the wire.\u003C/p>\u003Cp>Right? We've got 6 minutes and 55 seconds here. How do we actually render out this lesson content? We are going to do, let's rely on chat GPT or get up copilot. Right?\u003C/p>\u003Cp>Function to generate YouTube embed URL. Okay. So now we're gonna get youtubeurllesson.video. Let's see if this actually works. Cannot read properties of undefined split.\u003C/p>\u003Cp>So we'll do if, oh, I know what it is, we've used video URL instead of video. A lot of these issues can be avoided by using TypeScript, but lesson let's do a v f. Cannot read properties index of undefined. YouTube video ID. So we need to take a look at Directus.\u003C/p>\u003Cp>It's what's fun about doing these things live. You never know exactly what's gonna go wrong. And when you're under the gun, the pressure is mounting. Don't necessarily know. Can't even get the password right now.\u003C/p>\u003Cp>Hallelujah. There we go. Alright. So in this case, we want to, accounting for YouTube URL variations. Let's see what this thing comes up with.\u003C/p>\u003Cp>And you to that be Will this actually work? Hey. There we go. Alright. We probably want to do, like, an aspect ratio, aspect video.\u003C/p>\u003Cp>Okay. And so now we have our content. We could do a v if, so there is no lesson content here. And boom. So now if I click through these, we should be loading our different courses.\u003C/p>\u003Cp>So at this point we are at 3 minutes and 44 seconds. So we did not get as far along as I thought we would get on the front end of this. But, going in and extending this further, one of the things that I would do next is probably taking a look at building this out and adding enrollments to this. So we could set up a like authentication for this so that, let's just say on the access control side of it, instead of allowing all the courses to be seen we could certainly let people view all the courses, see the instructors. But as far as the actual lessons, maybe we didn't want to give them access to that until they were enrolled.\u003C/p>\u003Cp>But for anybody that was enrolled, they could see all of those courses. So now if I refresh the page, we're gonna see like an error that says, hey, this is forbidden because we cannot see this actual course. But if I were to go into my authentication page, let's just say auth/login and use my Directus URL or my Directus username and password. Go back a couple pages. Where are you?\u003C/p>\u003Cp>Courses. Alright. We go into that and we can actually see those. And that's because this user is that user is now authenticated as the administrator and they have full rights. But we could set up a public and then a user specific role where we've got courses, we've got instructors, you can see all of that, where we want to see only the, like, courses that that person has access to.\u003C/p>\u003Cp>So, let's just go with no access, like modules, that's fine. But as far as the lessons, you would have to set up like a a role here or, like a custom permission for this specific user. Taking that a step further, you know, we could get into like, making it work with Stripe, setting up some of the different, payment options. Like if we took a look at Udemy, there are, you know, trial options. You could pay individually for courses.\u003C/p>\u003Cp>You could set up subscriptions for this. But, clearly we we built the back end of our LMS, but, really a little disappointed on how far we made it through the front end, considering I've actually built several of these systems myself. But we've definitely learned a lot. So if we take a look at our list of functionality, how do we do? We got to view a list of courses, we got to the individual courses, we got the individual lessons, We did have the authentication and login, but as far as tracking progress, and enrolling for a course, I give myself, what, 4 out of 6 or 4 out of 7.\u003C/p>\u003Cp>Not a stellar effort here. So that is this lesson of 100 Apps, 100 Hours. I hope it's been a great example of how quickly you can build functionality like an LMS inside direct us and with front end tools. But there is a reason these other products exist like Podia, Kajabe, Udemy, Teachable, all of those. Rome wasn't built in 1 hour.\u003C/p>\u003Cp>So hope to catch you on the next episode. That's all I've got for you on this one.\u003C/p>","Welcome back to the next episode of 100 apps, 100 hours, where we try to recreate and build your favorite apps in 1 hour or less, or die trying. Hopefully not dying. I'm your host Brian Gillespie, developer advocate at Directus. Super nice to have you. Today we've got a app that is near and dear to my heart. I've created a lot of courses in the past, so we are going to be building a learning management system, an LMS. There's a few examples that I took a look at in the the research for this prior to. I've got my proper dad attire on and we are going to dive in and build some apps. So let's just add a little bit of context for you guys before we start the clock. If you take a look at sites like Kajabe or Podia, these are online learning management systems. Most of these are software as a service where you can sign up for, $159 a month, it looks like, or $199 a month and sell your courses. The interface for these, if we can maybe navigate to one of, Podia's websites here. We've got like a main navigation on the left. We've got, you know, the courses that show up on the right. If we take a look at Udemy, that's a kind of a different case where you're on their platform, you're not white labeling, but you can create your own courses and load those up and sell those to their specific audience. So I think we'll land somewhere in between all of these different tools today. Should be very fun, very interesting. So with that, let's, let's dive into this. I'm gonna start my little mouse pose thing so I can highlight things on screen as we go through and build. So, I want to kind of show you what I am working with just so there is no, you're aware, there's no cheating behind the scenes here. I do have a Directus instance already started. I haven't logged in yet, but that is a blank Instance ready to go. That's what we'll use for the back end of the app. We also have a Nuxt 3 starter application. I do have like a Directus SDK already preconfigured and a couple of utilities just to make building a little bit easier. But aside from that we can see this is just a blank slate. There's a login form, text component and an upload component, just to make this a little easier. A lot of this is boilerplate functionality, I don't want to bore you guys with that. So that's the setup. Alright. Let's dive into building the app and we will start the clock on this. So we now have 60 minutes to develop this out, But before we actually do any code, I want to like sketch this out. And I I love using Figma for this. So the first thing we want to flesh out is our data model. What's our schema? We're going to have courses. So within each course, we have some lessons. We probably have some modules. Alright. Maybe these are not huge per se. We'll just make them smaller. What else are we gonna have? We're gonna have users. We'll probably have some instructors. Right? We want to be able to track the instructors on the course. This is probably a a pretty good starting point. Now as far as the functionality that we need, so let's just call this our data model. Cool. We'll go over here. Inspect this out. Functionality. Alright. Let's go through and flesh this out a little bit. We want to view a list of courses, view an individual course, and this is gonna be on the front end. View individual lessons, we probably need to sign up for our course. We just walk it through the entire process. View individual lessons. We want some authentication, so we wanna be able to log in. And, what else? We wanna be able to track progress. Right? And, to that end we probably need like a collection to track enrollments. So who are our users, what courses are they enrolled in, what is the completion percentage for those. Alright. So this seems like a pretty good start. We'll continually come back and refer to this and refine it as we go through. But the first thing that I wanna do is start into our back end. So I'm gonna pull up Directus in this case, and let's see if we can zoom in just a little bit to make it easier for everybody. Now I've got this running on Docker locally, but the easiest way to run Directus is using Directus Cloud. And I need to make sure I've got the correct password here. Alright. So we've got a blank Directus instance. We're going to flesh out this data model in no time at all. So let's create our 1st collection, we'll call it Courses, and we will use the generated UUID as the primary key field. We'll just go in and for now we'll just select all of these defaults for tracking date created, user created, etcetera, handy to have. And for our courses we probably want a name for the course, that's great. We want to have a description for the course, so that will be a WYSIWYG editor. That way we could support HTML and rich text formatting. What else do we need as as far as our course data? We've probably got an image for the course as well. So we could call this image. We could have multiple images, but let's just stick with a single featured image, and we'll use that. Great. So now if we back up, we've got the start to our courses data model. Let's go in and flesh out the other parts of our course. So each course will be made up of several different modules. So the modules, again, we can add all these fields. We may not need those. I typically just add these because it has some preset functionality. Obviously the user created it will store the UUID for that particular user, updated date and times, those are all handy to have. So the modules, we will have a name for the module, and, you know, we could have like a short description for the module. Let's just stick with name, we'll keep it really simple. The other thing that we may want to have on the courses, this just reminds me, is maybe something like a slug. So just a pretty URL for that specific course. So we'll go in and set that up. One of the other things I could do really quickly in Directus is make sure this is URL safe. So there's just a, a field to set that up for us to automatically format that. Next, let's go in and add our lessons table, or lessons collection. We'll do the same, so what's the status, is this published or is it draft? And now in the lessons we'll have a name for the lesson. A lot of names here. We'll have the, let's call it Lesson Content, and that could be rich text. And because these are primarily videos, let's do a video URL. We can host those on Vimeo, YouTube, Bloom, however you wanted to do that. You could also just actually upload these into Directus as well. Alright, so now we've got kind of this side of the equation figured out. Directus already gives us users out of the box, which is great. We can go in and add these other collections. We'll have an instructor's collection. Instructor's, I hope that's how you spell it. So our course instructors, that will be we'll have a name for them. Let's have a bio, so that'll be a WYSIWYG field. And one of the things that I love most about Directus is just how easy it is to map out this data model and get a complete back end with an AI or API ready to query for my front end. So instead of working with dummy data, I can actually prototype live with real data. So we got our name, we got our bio, let's do an image, or we could call it avatar. Great. Cool. And last but not least, we've got enrollments. So we'll just, create all these separate tables, all these different collections within Directus. And now we can go back and flesh out the relationships between these, because that's where the tricky part comes in. Or not necessarily tricky with Directus, but, we wanna make sure that data model works correctly on the front end. So the when we start mapping these out we have our courses. Each course could have multiple modules, and each module could have multiple lessons. So those are one too many relationships inside Directus. So if we start fleshing this out, we'll go into the Courses collection and we'll use the relational fields inside Directus. So behind the scenes Directus will set up these relationships inside your SQL database and make sure everything plays nice with each other. So we're going to call this modules. So that's going to be our key. The related collection here is going to be modules. And then the foreign key, so our key inside the modules collection is going to be Course. We could choose whether we want a list or table layout, that's fine, and we'll just use the name of the module and we'll show a link to that module. So now our modules are set up and if I were to go to the actual module section we can see we've got the reverse relationship back to the course. And maybe we drag and drop this, give these some icons to make them look nice. So we've got a Course, maybe that is a folder looking object. It's great. Let's use black as the color. So we'll customize this a bit and make it look nice. The module, you know, let's look for a list, maybe. Looks great. And now let's add a lesson, we'll just use like a video icon for that. Perfect. Okay. So now if we go back to our modules, again we're going to create a one to many relationship, but this time we're going to do that on our lessons. So the key here will be called lessons and the related collection will be lessons. Our foreign key will be module. So just the singular because each lesson can only be in a single module. And we'll use the name of that lesson. Great. We'll show a link to the item. And now we've got that structure set up. Great. How do the other items relate? Right? We've got instructors. Instructors could have many different courses inside our platform, and a course could have multiple instructors. So that's a good use case for the many to many relationships inside Directus. And what we'll do is go to either courses or instructors, doesn't really matter which one. And instead of a one to many or a many to 1, we're going to use the many to many relationship. So here we'll call this instructors, that's gonna be our key. For our related collection, we're gonna use instructors. And we don't want to allow duplicates, we want to show a link to the item. If I wanted more control over what Directus creates, or uses for the name of the junction tables, I can go into the advanced field mode. So we can see in our junction collection Directus is going to create this inside our SQL database. Courses underscore instructors, and there's gonna be a Courses ID and an Instructor's ID. I can also add this relationship to the instructors field and we'll give this a sort just in case we want to have a head instructor and, you know, prioritize who is the primary instructor on the course. So we'll hit save, Directus will do some magic behind the scenes, and if I go in now I will see a hidden collection in our data model for courses underscore instructors. Savvy? Everything's looking good so far? Great. Alright, now we also want to flesh out our enrollments. So if we think of enrollments, enrollments are going to be the users that are enrolled in the individual courses and this setup will be a little bit different in that we go to Enrollments, there's gonna be a many to one relationship. So each enrollment can only have one course. So what are the course that this person enrolled in? Great. And then we're also going to have a many to one relationship with the Directus user. In this case we could just call it user as well. But the related collection here is going to be the Directus underscore users. That's our system collection for users within directus that, gives us all the authentication and user access and permissions. We could also keep track of progress or lessons completed, but for now let's just roll with this. Okay, a little OCD over things like icons and colors, so I will add some icons and colors here. We've got instructors. These will be people. Let's see what we've got. Nice friendly guy waving, that's perfect. Let's make that purple as well. Alright, so now if we look at our data model, this is looking pretty good. We will go into the front end and we can see that we've got courses, we've got modules, we've got lessons. Let's just create a course. Let's do published. And we'll call this 100 Apps in 100 hours, great. Watch to follow along as we build an LMS. While building an LMS? I don't know. It's kinda meta. No worries. Alright so we can, let's go to Unsplash and find a nice looking image. One of the nice things about Directus, and you'll hear me say that multiple times throughout this series, is the ability to import from a URL. So I can just copy that URL there, import that image, it will store it in my file library for me. And we'll give this a slug. So you'll see if I press space bar it will automatically format this for me so that it becomes URL safe. And let's go in and create a module. So we'll call this 1st module. We'll create a second module. Great. And we'll add an instructor. The dude building stuff and videos. Cool. I've got let's upload a photo. Great. Good. Solid. Okay. So now we've got our first course. Since we've set up these collections I could start querying this on the front end using the Directus APIs. So So if I just open this up in a new tab, I'm going to change the URL a little bit. We'll go to items/courses and boom, we get this permission access error. So I haven't enabled permissions. We've created this actual, collection. We've created several of these collections, but for the general public all these collections are access is forbidden by default. So I could open this up and you know, just allow public access for now to all of these. Once we went to production, obviously we want to restrict this per user. But if I just refresh you can see over here on the left, excuse me, we've got our data coming from the LMS, or the back end of our LMS, our direct us instance. Great. So we are about 15 minutes in. We've got our first bit of data modeling done. Let's dive into kind of the front end and start building, right? We will, I'm just gonna move this out of the way for now, and we'll get to a blank slate inside our code editor. We've got our direct assistance up here, but let's pull this up. Now within the pages directory of my Nuxt application, maybe we create a new folder for courses. This will give us a route slash courses on the front end. And we'll have an index route. That's great. We'll set up a default view component here and now we can start querying that data from Directus. One of the things that I do have in my little Directus module is a composable to actually query the front end of or query the Directus back end. So we can do something like this where we say const courses equals useDirectus. And this is using the Directus SDK on the the back end. We're gonna read items, courses, And I could go in and add some options here as well. You know, Copilot is showing me like a filter for published. But for now, because we haven't published anything, let's just take a look at this. We'll, log this out to the front end. And if we navigate to /courses, it looks like we have an object promise, so we wanna do await use directus, and boom, now we've got our data on the front end. Cool. So now we probably want to do a little bit of formatting, you know, add this, add maybe a card for this. One of the things that I am using or I've got set up in this Nuxt instance or this Nuxt template app is the Nuxt UI library, which is just a UI library put out by the Nuxt team. It helps you build apps that look great faster. It works perfectly for stuff like this. On a lot of other projects, I may use, you know, my own custom coded components. But for now let's do a card. We'll do v4 courses and courses because that is an array, course ID. Let's actually just do something like this where we've got v text. I do also have a text component, we'll call it course. Name. And we can use the Nuxt image component, course. Image. See how far that gets us. Okay. So we've got a card. We probably want to give a little bit of styling to the container. What is the I guess the muxu container It constrains the width. Okay. And then maybe we've got let's just go back. We'll do the keep this really simple. I also, use tailwind a lot. We'll do like a max width of 4 XL for now. Make this in x auto. And maybe we set these up in a grid with 3 columns on, like a large size. Okay. So we've got this actual course card. We're not rendering the actual image, which, is concerning me. You don't have permission to access this. So we've hit our first kind of snag here. Somehow I've managed to log myself out of Directus as well. So let's log back in. Directus. Alright. And now we will go in and we forgot to set permissions for our system collection. So all the Directus files, we're just going to make those public for now so we can take a look at that file. I could now see that and it should show up within our card as well. Great. You know, maybe we make this font bold, 2 x l. Make it a bit bigger. Let's make it giant, right, 4 x l. I need to take a look at my Vtex component actually. I think I've already got sizes on here, so we could just use the size prop instead. 2XL. Great. Okay. Alright. So now we've got kind of an index page. This is a course listing. Let's wrap this with, another div. I'll go in and paste that. So now we've got a course listing, we've got a course card, How do we get to this actual course detail? So I will go in and, now we'll go into our courses folder within the pages directory And let's create actually let's create a new folder. And we'll use the brackets for this. This will give us a dynamic route. And we will use like slug or I could call this course or ID. We're using slug as the property for the course on the directive side, if I remember correctly. So we've got a slug for each of the courses. That's what we'll query by. Sounds great. And we will go in and now we will add like an index page for this course. So again, we'll build a detailed page here, just a simple index page, and let's do course, wait, use direct us, read item. We wanna read a single item. This is using our SDK context. And we'll use route params slug. That's not gonna cut it. Actually, we want to read multiple items. And the reason why is because we're giving each course an ID, a a UUID. And the Directus SDK, when you read a single item, you can look up by that primary key, in this case the ID field. But where we want to look up by the slug, we're going to use read items and then we're going to construct a filter. So it'll be like this where slug is equal to route dot params.slug. We're gonna use route. So we'll pick up the route from view. We use route dot params.slug. And again, this is why I love building this way with the back end first because I can use actual data to flesh this out. So now on our course listing page we want to add a link to that. So if we go back to the index page, we can wrap, both of these in a Nuxt link component. And for the to prop, let's just add, courses slash course ID or no, course dot slug. Alright, so now we should get a clickable link. We can see at the bottom of the screen it says 100 Apps, 100 Hours. And when I click on it it will give me, an array of the data in this case. But I would rather have just a, I'd rather have the actual image. So let's do something like this where constant course equals courses dot I or core the first item of the array. Maybe this could be a computer prop as well just in case that changes. Courses dot value cannot read properties of undefined. What is going on here? Let's back up and let's just keep it simple. Right. Course equals courses. Okay, there we go. So we've got our course, and now we can start fleshing this out. So if I go in and I look at the Nuxt UI Library, we've got some components here, we've got like a skeleton. We've got a container. We've got a card. What do some of these other apps look like? How do we actually get to their courses? Not a lot of great examples on the website. If we look at Udemy, we could see we've got like a course name, we've got a preview of the course, we've got, some of the lessons here. And then when you get into the actual, enrollment of that specific course, they've got like a kind of a sidebar layout. So let's create a new section. We'll do the course title, course description, follow along as we build. Okay. 100 apps, 100 hours. So the course description, we're gonna use v html for that. Course dot description. Okay. And now we've got that. We could apply some nice styling for this. Let's do the font. Black text 4 x l. We'll do a, let's try that U container again. And this is coming from the Nuxt UI library. Alright. So we got that. We've got, the description. Maybe we wanna add like a small bit of text to label that. This description class text extra small, text gray 500, something like that. And we can space these out a bit. So again, this is a bit crude. We're going for speed over beauty in this case. But we want to flesh out this functionality. Where did you go? Okay, so we got our course name, we got a description. Let's show our instructors. Alright. So this is gonna be our pclasstext. Let's do instructors. Okay. So we've got that. We'll add a bit of margin to it. Space it apart. We'll pull up the instructors. How are we doing on time? Quick time check. Got 30 minutes remaining to flesh this out. Gonna be a definite challenge here. So we've got the instructors. We'll do, should be course dot instructors, right? Let's just log that out and see what we get. Course dot instructors. So you can see Directus is giving me back an array of the different ID's for those instructors. I've said this multiple times, but this is one of the reasons why I really love Directus. Because I can go in and for, this is using the REST API. I can actually go in and do something like this where I say, hey, I want all the root level fields. And then for instructors, I want, I want a list of all the root level fields for the instructor. So I could get all the related fields in a single API call. Now where this is a many to many relationship, you can see I need to drill in one more level. So I have to go in and do one more level where I have instructors underscore ID and grab all those fields. So now I can see the instructor and we can set up like a, just a v four loop here. V four instructor in course dot instructors. And it looks like Copilot has added a lot of code that I did not mean to add here. So let's just pull that back. Be careful how many times you tab, right? So now let's take a look at instructor within that loop and see what we have. Are we even getting anything? Alright. We've got instructors. We've got the instructor ID. K. So we could do something like this. We could actually destructure that. Or can we? Instructor instructor's ID. And let's see what we get back with that. We're breaking stuff. Okay. Yeah. So we'll just run with this for now. Alright. So we've got our instructor, we've got an instructor ID. So we'll have instructor dot instructorsid.name. Let's wrap that P TECH. We'll do, Nuxt Image. And let's do, yeah. Maybe width 24 sounds good. 24 height, rounded full to make this a circle. And then the source is gonna be instructor dot instructorsid.avatar. And then we can do a divvhtml for the bio equals instructor dot instructorsid.bio. Okay. Cool. So now we've got a few things going on here. Let's actually look at the lessons. Right? We'll go into our Directus instance. If we go back to our specific course here, we didn't add any lessons to this specific module. So I'm just gonna pull up the Directus YouTube account. Our YouTube studio here will get to the Directus channel and take a look at our content. So I'm just gonna copy a few of these down. We're gonna add a couple of lessons within each one of these. Just gonna actually take these wholesale from the YouTube channel. Here's some content. Great. Alright. How do we do, we'll copy some of this. How to manage different versions. Blah blah blah, copy that video URL. Oh, that's actually a short. Not sure what that'll do. Let's, let's take an actual video. Great. So we've got our first module. Let's go into the second module and we will add one of my videos. Getting started with Agency OS. My wife tends to make fun of, my sausage fingers all the time. So lots of typos here. So we'll go in. Let's just copy one more of these so we've got something to look at on the front end. Project templates. Great. Okay. So So save this. Now we've got our course, we go back to our course detail page in this instance and, you know, let's add a new section over here where we've got our section. Looks like it get hub copilot for the win. We've got our lessons. Give that some margin. Give some space. And, so if we log each lesson, let's just take a look, we'll probably get the same, let's just close this out real quick. We'll do course dot lessons just to see what we're getting back from the API, which it doesn't look like anything, right? So if I open up the dev tools, we take a look at the network request, we dive into the data, the lessons are actually going to come through the different modules. Alright, so again, we can go back up to our fields section and we could do something like this where we go to modules, then we, drill down again into, our lessons. And we want to grab the module name. And within our lessons, again, we could just grab all the root level fields within that. And now we can see that second API call that was made. We've got our modules. We've got our lessons within that. And again, I only have to make a single API call. You know, we can certainly cache this if this data doesn't change, but it keeps the entire app very snappy and prevents me from having to make 35 different calls to get all the related data. Also kind of it follows a very GraphQL like structure as well where I can request just the data that I need. I don't necessarily have to go in and use these wildcards to grab those root level fields. I could go in and say something like name, description, that way I can prevent over fetching and larger network requests than I actually need. So we've got, now we've got our data coming in correctly. Let's go through and we're going to loop through the modules. So we'll do v 4 module in course dot modules, and then inside that we'll have another loop for the lessons. So we've got our second module. Let's style these a bit. So maybe we wrap this, do something like divide y and divide y, divide gray 300 maybe? Okay. So now we've got a divider between our different modules. And maybe we give each module some padding. That's probably a bit so we'll do p y 4. Let's style the module a bit And font, let's just give it a little fancier font. I think I've got this set up as maybe Poppins inside my specific account. Font display. Great. Div 4. Okay. We got the we don't really need the lesson content. And then let's style each one of the lessons. You know, we'll likely change these to a NUCs link and then the 2. Now we want to dive into each individual lesson. So our URL structure is going to be courses, we've got course, so we use the dollar sign, so so we get the template literals here. We've got course dot ID and then we've got lessons and then maybe lesson dot ID or it we could use slugs on the lessons as well if we wanted to. Let me fix this. We've got slugdot ID. Okay. Unterminated template literal. Okay. Alright. So now we've got a URL that should take us to this specific lesson. I'm gonna go back into our Directus instance, which keeps logging me out, probably a cookie or some type of setup issue on my end when I configured Docker, but we'll work around it for now. Let's go in and give a slug for the individual lessons as well. So, just to maintain parity. We'll go into the interface, we'll make slugify checked, and now we've got a slug for the lessons. Let's go into each one of those lessons and set a slug. Project templates, managing versions. Alright. Getting started. And custom operations. Okay. So now each one of those have a slug. We can change this from ID to slug. We'll still use that ID as the the key for looping over these. But now we're starting to get something that kind of looks like, some courses. You know, maybe we wrap these in a go back. Maybe we wrap these in a card component. And a lot of times as I go along, I would definitely be refactoring these as we went along into components that made sense. So we've got the padding, p y 4. Let's do some space between each card. So we've got a little bit of space between each card. That's great. There's our different modules. I don't know why the first module is showing, below the second module. That could be the way that we've got our sorting on the course. So if we go in, we don't have sorting enabled. So what I can do is go into our Courses section, our Courses Data Model, we'll go to the relationship field, or the relationship tab, and for our sort field we're just going to add that sort property, or that sort field on that. And that should take care of the sorting for us. So now we've got our first module, that's great. We've got our second module, starting to look like something. Let's just take one moment and make a, like a nice little header for this. We've got image course dot image, object cover, And we'll give this a little bit of padding as well. MT 8. Maybe we want to round the corners. Again, very crude but, definitely paints the picture. So we've got about 18 minutes left. Let's dive into the actual lessons, Right? So a pretty common set up when we go to the lessons, and right now we're getting a Page Error Not Found, is to have a list of the other lessons over here on the left hand side, and then on the right hand side we've got the actual course content. So let's figure out what that would actually look like inside here. So one of the nice things about Nuxt is like the nested routes. So we go to their documentation, we go to, it's like child routes. Child route keys, nested routes. So I can set up, like parent and child collections, or pages, within these nested routes so that, it's a nice way to do routing, where I can have a child page show up in the parent view. So let's go in and we've got our slug here. So this is our actual course. Within the course, maybe we want to show let's do a slug. I don't know if this will be it or not, slug dot view, So we'll create a new folder for lessons and then we're going to create a dynamic route for the lesson. So we'll call that lesson dot view v comp ts. We're going to use the route, so route equals use route. And here I'm just gonna log the params to make sure we're on the correct spot. Alright. So we can't find the slug dot view. That's great. Let's add a component for that. Courses, 100 apps, lessons, managing versions, slug. Let's just say this is the course parent. What does that get us? Alright. So we've got managing versions. We don't see the course parent part of it here. Okay. So let's call this lessons. And now all we should see is course parents. But if I go in, and I can't remember the exact syntax, so let's go back to nuxt.com. We'll do the child routes, nested routes. It is what? NUXT page. So we just throw this NUXT page in here And now we see this where we have Course Parent, and then we have our NUCs page which gets rendered inside that. So why are we doing it this way? Because we can actually fetch the lessons within this parent component and then, that will not re render, but then we can navigate within these individual lessons and render those over here on the right. So if we do something like this where, let's flesh this out, we've got a, let's do a div. Alright. We could do, like, an aside maybe. This is the list of lessons. Then we've got another div. We'll render that, and we'll just flex these. Flex. We'll add, let's just maybe give this a fixed width of like 56 or 64 wide. Alright. And just to illustrate this, we'll give it like a background, a bggray100. So we've got our list of lessons there. You Gonna add some padding for that, p 4. Then our next page, we probably got a new container. I need to actually look up order the the settings for that. Let's take a look at it. You container, constrain the width of your content. Max width 7 x l. Okay. Yeah. Works fine for me. Alright. And this could be probably height full. We want it to be the full height or height, screen. That'll get us. Cool. Alright. So now we can go in and render the list of lessons here. So we will grab our lessons. So let's do const lessons equals await. You got GitHub Copilot for the win. We're gonna read the items from the lessons. The filter is gonna be where the course dot slug equals route dot params.slug. So we're gonna pick up the we can pick up the course from the course slug. And let's actually see what we get here. I refresh the page. We're breaking some stuff. Course, slug, route. Oh, we actually have to grab the route, don't we? Forbidden. So I can't get this information. Wonder why that is. Let's take a look. We'll just erase that filter, see what data we're getting back, in this list of lessons. So I'm gonna quickly wrap that and, just output the lessons. Okay. So there's our individual lessons. Looks like those are actually coming through. Okay. But if we go to course dot slug is equal to, Let's just take a look at our network request. Alright? This is being fetched on the server side. You don't have a permission to access this, which makes me think something is wrong. Alright. So let's just look through this really quickly to see if we can find our module. Duh. That's because our modules are not, our lessons are not actually linked to our course, they are with our modules. So the the courses belong or the lessons belong to the individual modules. So, fun debugging issue here, but let's just go into our original index where we get the course, we get our lessons, and we can actually copy this code. And within the lesson route, her lessons where's our parent component? We could do this. Right. And here we could probably adjust it where the slug or the course, slug. So we get all the modules instead of the lessons, which is a little bit counterintuitive. But we still wanna display all those modules within that bar anyway. Alright. So we get our modules. We'll change this to modules. And we'll make sure that we grab the lessons as well. Let's just clean this up and refresh. Something is breaking. We don't get our modules. Route is not defined. Again, silly me, silly rabbit, you have to use the route. Alright. So now we can loop through these modules. And what does GitHub Copilot got for us? Alright. So we've got our different modules here, how to manage different versions, how operations can be completed. Okay, great. Nuxt link courses, route planarams. Slug. And now if I am navigating between these over here on the right you can see that this is actually changing. You can see that we're logging the slug for that specific lesson. So if we go into the lesson, we've got route params. Lesson. Now within this specific component we will call the lesson that we want. But what we'll do is make sure that we update this a little bit. And we're gonna use the lesson equals lessons if we just log lesson over here. Alright. Boom. We can see that. If I go back, I wanted to make sure this doesn't shrink. Select shrink. 0. Alright. And now on our individual lesson, let's actually set this up. So we got the lesson name. We got the lesson content. It's great. And then we will have a where it'll be an iframe? For the YouTube Embed. And I actually think do I have do I have Do I have a video string for this? I don't. Generate lesson and video. What is the YouTube embed? Alright. So we're getting the URL from YouTube. So if I log in to the lessons here, how do we transform this? Closing in on what? We're gonna cut this one down to the wire. Right? We've got 6 minutes and 55 seconds here. How do we actually render out this lesson content? We are going to do, let's rely on chat GPT or get up copilot. Right? Function to generate YouTube embed URL. Okay. So now we're gonna get youtubeurllesson.video. Let's see if this actually works. Cannot read properties of undefined split. So we'll do if, oh, I know what it is, we've used video URL instead of video. A lot of these issues can be avoided by using TypeScript, but lesson let's do a v f. Cannot read properties index of undefined. YouTube video ID. So we need to take a look at Directus. It's what's fun about doing these things live. You never know exactly what's gonna go wrong. And when you're under the gun, the pressure is mounting. Don't necessarily know. Can't even get the password right now. Hallelujah. There we go. Alright. So in this case, we want to, accounting for YouTube URL variations. Let's see what this thing comes up with. And you to that be Will this actually work? Hey. There we go. Alright. We probably want to do, like, an aspect ratio, aspect video. Okay. And so now we have our content. We could do a v if, so there is no lesson content here. And boom. So now if I click through these, we should be loading our different courses. So at this point we are at 3 minutes and 44 seconds. So we did not get as far along as I thought we would get on the front end of this. But, going in and extending this further, one of the things that I would do next is probably taking a look at building this out and adding enrollments to this. So we could set up a like authentication for this so that, let's just say on the access control side of it, instead of allowing all the courses to be seen we could certainly let people view all the courses, see the instructors. But as far as the actual lessons, maybe we didn't want to give them access to that until they were enrolled. But for anybody that was enrolled, they could see all of those courses. So now if I refresh the page, we're gonna see like an error that says, hey, this is forbidden because we cannot see this actual course. But if I were to go into my authentication page, let's just say auth/login and use my Directus URL or my Directus username and password. Go back a couple pages. Where are you? Courses. Alright. We go into that and we can actually see those. And that's because this user is that user is now authenticated as the administrator and they have full rights. But we could set up a public and then a user specific role where we've got courses, we've got instructors, you can see all of that, where we want to see only the, like, courses that that person has access to. So, let's just go with no access, like modules, that's fine. But as far as the lessons, you would have to set up like a a role here or, like a custom permission for this specific user. Taking that a step further, you know, we could get into like, making it work with Stripe, setting up some of the different, payment options. Like if we took a look at Udemy, there are, you know, trial options. You could pay individually for courses. You could set up subscriptions for this. But, clearly we we built the back end of our LMS, but, really a little disappointed on how far we made it through the front end, considering I've actually built several of these systems myself. But we've definitely learned a lot. So if we take a look at our list of functionality, how do we do? We got to view a list of courses, we got to the individual courses, we got the individual lessons, We did have the authentication and login, but as far as tracking progress, and enrolling for a course, I give myself, what, 4 out of 6 or 4 out of 7. Not a stellar effort here. So that is this lesson of 100 Apps, 100 Hours. I hope it's been a great example of how quickly you can build functionality like an LMS inside direct us and with front end tools. But there is a reason these other products exist like Podia, Kajabe, Udemy, Teachable, all of those. Rome wasn't built in 1 hour. So hope to catch you on the next episode. That's all I've got for you on this one.",[226],"0ea8d80f-1177-49fd-b106-ff30b3efb903",[],{"id":172,"number":131,"show":122,"year":173,"episodes":229},[175,176,177,178,179,180,181,182,183,184,185],{"id":177,"slug":231,"vimeo_id":232,"description":233,"tile":234,"length":235,"resources":8,"people":236,"episode_number":143,"published":201,"title":238,"video_transcript_html":239,"video_transcript_text":240,"content":8,"seo":8,"status":130,"episode_people":241,"recommendations":243,"season":244},"swag-platform","894282937","Gifting platforms are how we get goodies in the hands of community members and customers. Bryant needs to figure out how to manage products, build pages to handle the giveaways, and manage fulfillment.","9c659211-390a-4bfa-afd4-21fd08c80fbe",67,[237],{"name":199,"url":200},"Mission: Swag Platform","\u003Cp>Speaker 0: Alright. Alright. Alright. Welcome back to the next episode of 100 Apps 100 Hours where we rebuild or build some of your favorite apps in 1 hour or less or die trying or publicly fail trying is probably more appropriate. I'm your host, Brian Gillespie, developer advocate at Directus.\u003C/p>\u003Cp>Welcome. Super excited to have you. Today, we are going to be building a gifting platform. Oh, Brian, that sounds really exciting. Actually, to me, it is.\u003C/p>\u003Cp>So let's dive into the details of this. I've recently gone through at Directus and set up a swag program. So we've got tons of users worldwide that use our software getting, merchandise like branded cups or mugs or t shirts, into the hands of our user base, incredibly difficult challenge when you consider that we've got users in the US, we've got users in Germany, France, Namibia, just every country that you can think of. We've got people that we would like to thank and show appreciation for. Very challenging.\u003C/p>\u003Cp>So inter gifting platforms. We are going through the process currently with a company called swag.com. They offer a shopping cart and e commerce experience where you can log in and, basically pick your swag, upload your designs, and place orders that they will then fulfill for you. There's quite a few of these types of companies like Sendoso, I found another one called SwagUp, and this one is Codus Design. It basically a swag platform where you place an order, they manage all the fulfillment and inventory and actually delivering those items to your clients or your customers or your potential leads, potential prospects.\u003C/p>\u003Cp>They handle all of that for you. The part that I was very interested in, and I'm not discounting the fulfillment and warehousing and all the other bits and bobs that are included in here. That's probably the really difficult stuff. But, our past ecommerce or past swag solution was a Shopify store. And when you're trying to send somebody a thank you gift, it feels a little weird to have them check out on a Shopify store and enter a coupon code to actually get that get that swag or get that gift that you're trying to send them.\u003C/p>\u003Cp>And one of the reasons why I really like these platforms is the ability to send out a thank you page that looks very similar to this. There's there's no checkout. They don't have to enter a credit card or a coupon code. I send them a unique link. They can pick their swag, and we get them to enter in their information like their first name, email, country, and address so we know where to ship it.\u003C/p>\u003Cp>It is a great experience for them, great experience for us, win all around. That's what we're gonna be building today. Let's dive in. Just a quick recap of the rules. There's 60 minutes to plan and build, so that's plan and build in 60 minutes no more no less, And aside from that there are new rules.\u003C/p>\u003Cp>We'll use whatever we have at our disposal whether that's AI or all sorts of other cheat codes from around the Internet. Let's get started. We're gonna dive in. Alright, so before I build any apps, I usually like to take at least 5 minutes to plan. These planning sessions are always fun because it's hard to test your assumptions to get feedback when you have 5 minutes to plan an app.\u003C/p>\u003Cp>But here is my initial functionality that we're gonna build into this app. We're gonna need to manage our products and product options. We will need to, create a giveaway page or several different things that the the companies that I looked at called this, like a redemption page. I think giveaway is probably a good one. Thank you page, create a giveaway, a redemption page.\u003C/p>\u003Cp>Place an order, place a request. Not sure it's a order because it's free, but hey, that's what we'll do. User places a request, k, let's say user can place an order request, and then we manage and fulfill those through the app. Alright. So that's the app that we're gonna build.\u003C/p>\u003Cp>There are 2 components. There's a back end. There's a single page on the front end. What are we gonna need as far as our data model? What is that gonna look like?\u003C/p>\u003Cp>So we'll jump in and let's do, we certainly have products. Oh, I'm not ready to drag arrows yet. We've got our products. What is next? We've got our giveaway page, right?\u003C/p>\u003Cp>Giveaway. But then we have customers, you know, we'd probably wanna track that as a separate table or those could just be people. And then we'll have orders and probably some actual order items. So when we look at this, all our products roll up to a giveaway and we this is probably gonna be like a mini to mini relationship because we could have many different products on many different giveaways. When we go to products those are gonna be to the order items, customer orders or items will be orders attached to giveaways.\u003C/p>\u003Cp>I I just really like drawing arrows half the time. So as far as the rough data model, this looks pretty good. You know, curious to hear your feedback if if you set up ecommerce before, especially on how you handle like product variants and things like that. But let's dive in and not waste too much more time. Let's actually start building this.\u003C/p>\u003Cp>I'm just gonna move this off to the side. What does our setup look like as far as how we're gonna build? I've got a local Nuxt starter app set up. I've got a Directus folder where I've got a Docker Compose file that just simply creates my Directus instance that we're gonna be using on the back end. And then this Nuxt application just has a SDK for Directus already pre configured with a plugin using the the Nuxt plugins and, you know, nothing too fancy.\u003C/p>\u003Cp>The only, like, major difference here is there's a composable, view composable included for making the Directus requests, because Nuxt has that nice auto import feature. Alright, so let's log into our Directus instance and start building something right away. Let me look at what I'd use for my password here, just paste that and as we look let me get my data model up in view so we can actually start building. But as we look, this is a blank instance, I'm not doing any kind of crazy cheating here. Let's start building.\u003C/p>\u003Cp>So we'll create a new collection for products, we'll track some dates on this, you know, is this a draft product or not? Maybe we don't really need that for this particular instance. Alright. So on our products, what do we have? What are we using?\u003C/p>\u003Cp>We've got a name for our products, that's great, that's just a string. We've got a description for our product, We probably have some images for our product. So we've got some images. This is going to be using the relational fields inside Directus. And what's happening behind the scenes here as I'm building these collections and adding fields to it, the Directus API is mirroring this setup inside my Postgres database.\u003C/p>\u003Cp>And at the end of this, as soon as I get done building this data model, I'll have a ready to go API that I can then call on my front end to render those products. So we've got name, description, images, we're probably gonna have some options for our products like color and size. A more robust setup here would be to create separate collections for those, you know, a a product options and then maybe a product options values so that that could be dynamic. To streamline this, I think I wanna go with the repeater inside Directus. So this is just a JSON store and we'll call this options.\u003C/p>\u003Cp>So our options, we're gonna edit the fields here. The first option is gonna be the name of the option and then that'll be a string. We require a value for that, that'll be an input. It's fine. Let's make it full width and then next let's add values.\u003C/p>\u003Cp>So we're gonna get a little meta here and then I am going to embed a repeater within a repeater. Sounds pretty wild. So the field here, let's call it value. And I could probably have like a label and value in case the the value that I wanted to track was different than the label, but this looks pretty okay. We'll hit Save.\u003C/p>\u003Cp>We've got name, we've got description, we've got images, you know, if I had something like a SKU we can work on that, but let's keep it simple, right, let's keep moving. What's next? We have our giveaway. So this is gonna be a giveaway page, and if I pull up one, like, the direct to sample, we've basically got a logo, we've got a headline, we've got some description, then we have the products and we have a form. So let's add a table for giveaways.\u003C/p>\u003Cp>We'll go in and we'll set a status on the giveaway if this is published or not. Let's go in, we'll create a new field, we'll call it headline. We'll just make that a string. Then we have a description that we could set. So we'll do a description.\u003C/p>\u003Cp>Again that will be rich text so we could potentially have, lists or, you know, bold italicized text. And then we want to add our products. Right? So with our products, we're gonna use the relationship features inside Directus, and we're going to use that many to many relationship. Because one giveaway could have many products, and we could use many products on many giveaways on we use one product on many giveaways.\u003C/p>\u003Cp>So the let's call this field products. Our related collection is also gonna be products, and we don't want to allow duplicates. We do wanna show a link to the item so we can go back to that product. And I'm just gonna open this in advanced mode. So behind the scenes, what's happening when we use this many to many relationship?\u003C/p>\u003Cp>Inside the Directus or inside our Postgres database, Directus is creating a junction table called giveaways_products. Now I could adjust what that table is called, and even the foreign keys that it's setting up inside that junction collection, what it's naming those. But I'm just gonna go with autofill in this case. If I want to add the reverse relationship back on the products, so if I wanna reference those giveaways on the products themselves, I can do that here as well. And then maybe we add a sort field just to control the order.\u003C/p>\u003Cp>Maybe I want the shirt to show up first before the other items. Great. Directus is gonna tell me what all it's going to create inside my database, and now we've got our products. What else do we need? Do we need anything else here?\u003C/p>\u003Cp>The form, maybe we hard code that. Great. So if we look what else? We've got a customer, we've got orders, and order items. Alright.\u003C/p>\u003Cp>So let's go ahead and set those up as well. So we got a customer, actually let's just call it persons, people. Naming stuff is always the hardest thing. This may not be a customer. Right?\u003C/p>\u003Cp>So let's just go with the more semantic name and call it people. So people have a first name, they have a last name, they have an email address, and are we what else are we capturing? Capturing an address for those people. I'm assuming that could be on the order itself. Let's just roll with this.\u003C/p>\u003Cp>Right? First name, last name. Maybe we'll capture the address on the order. We could store it here as well, but we'll keep it simple. So we've got then we have an order and the orders are going to what?\u003C/p>\u003Cp>What are we gonna have on the order? The order is going to have a customer or a person, where we who's the person attached to that order, that's gonna be a many to one relationship. Alright. So we got the people, person, people, are already regretting this decision. No turning back now.\u003C/p>\u003Cp>Right? And then on the reverse side of that, on our people, we'll show the orders. People orders within the data model. Great. Alright.\u003C/p>\u003Cp>So the person we have, an address, let's just store that as JSON data for now. So we'll do an address and what else? We have a status is approved, maybe. This is approved, so we want approval before we ship all of these out. Let's add that to the top.\u003C/p>\u003Cp>And then last but not least, we want to create the actual items involved with this specific order. So we've got an order items table or you could call it orders items. Again, we get into splitting hairs here. What else do we need? We probably do need a sort for those blah blah blah.\u003C/p>\u003Cp>Alright, so we got our order, we got our order items. The items are gonna have a link back to the product. So we've got a related collection is gonna be the products. So when we put those items in the cart, the order item will have a product, so that'll be a relationship and then let's just have a JSON field to store our product options. Maybe we call it item options just to keep them separated.\u003C/p>\u003Cp>Good. And then there is a 1 or many to 1 relationship. So there are many items to one order, back to that order. So we've got the order, the related collection is orders. Tell me I didn't.\u003C/p>\u003Cp>I did, didn't I? So this is a boo boo. I've got orders here. It should be or I've got order should be orders. That should be a a fun little thing to work around.\u003C/p>\u003Cp>Now, I could go into the database itself and edit that if I wanted to, but, no worries. We'll just roll with what we got. There's an order. It's a order. Sometimes you goof up.\u003C/p>\u003Cp>What do you do? You keep rolling. Alright. So we've got our data model. Right?\u003C/p>\u003Cp>Looks great. Let's go in and maybe add a giveaway. And we can just duplicate this one, right? Directus Swag. Still learning Arc.\u003C/p>\u003Cp>I like it as a browser. Yeah. If anybody out there is a Arc expert, maybe you could give me a couple lessons like a tutorial session or something. Alright, so we got our headline, we've got a description. Let's add a product, we don't have any products at this point, so let's create a product.\u003C/p>\u003Cp>We've got the Directus, let's let's call it the Bunny Tumbler 20 ounce, Great. This is the most amazing tumbler ever. Great. Now we want to add some images to this. So, I can actually import images from a URL inside Directus, which is super nice, especially for speed runs like this.\u003C/p>\u003Cp>I just click upload a file, click the little link that will import our file. Looks great. For our options, we've got color and for the value, we've only got one. It's just white. We're gonna offer that in white.\u003C/p>\u003Cp>Great. Okay. Let's go in and add one more product. We've got our Directus logo tee. Kinda sounded like goatee, didn't it?\u003C/p>\u003Cp>This is the softest t shirt you'll ever wear, and again, we'll just go in and let's copy the image address and you can see we've got a couple options here. We've got extra small through 2 x, so we'll add those in there as well. Just upload our file. Great. For our options, we've got color, which is a dark heather.\u003C/p>\u003Cp>I believe I should know, I'm the one who ordered this stuff. And then let's add another option for what size. Alright. So we got extra small, we got small, we got medium. It'd be really handy if we could just do this in line, wouldn't it?\u003C/p>\u003Cp>I'm sure there's a way. Where there's a will, there's a way. Alright. So we've got some extra small through 2 x. We've got our products.\u003C/p>\u003Cp>We've added them to a giveaway. Let's go ahead and publish this giveaway and hit save. Great. So we've got our giveaway, and let me just prove it to you. Let's open up Directus.\u003C/p>\u003Cp>Alright. So this is our actual database here. We can see we've got order. We've got some directus tables that directus has created for us, and then we have giveaways. So there's our data.\u003C/p>\u003Cp>Directus prefixes all the metadata tables that it so it keeps the SQL database pure. That way, you know, you're not locked in or anything like that if you decide to migrate away. Very nice. Very nice. So now let's actually take a look at our giveaway.\u003C/p>\u003Cp>Right? How do we fetch this on the front end? And Directus gives us these REST APIs out of the gate. So if we go to Directus URL, which is local host 805 5 items giveaways, we get no permissions. So I have to go back and by default, Directus controls locks down all of my data.\u003C/p>\u003Cp>It wants us to be secure. So we've got 2 initial roles in an account. We've got a public role, which controls what data is accessible without authentication. And then we have the administrator role, which has unrestricted access. Now if I was gonna put this behind a login, I would probably do something like creating a user role that didn't have access or didn't have admin access, but they would log in on our front end.\u003C/p>\u003Cp>In this case, I'm just going to use the public role. We are going to allow people to see the giveaways, we're gonna allow them to view the giveaways, they can create orders and order items, they'll need to be able to create people, and we could also have, like, a server token, where we're doing some of this on the the server side as well. But we're not super concerned with security on this one because we've only got 40 minutes left to build this app. So we will allow access to see the products, see all the product files, and then we're going to go into the Directus Files under the System Collections and we're gonna allow that. So now if I go back, I open this up go to 8055/items/ giveaways.\u003C/p>\u003Cp>Boom. There's our giveaway data. Looks great. And we don't see any of our products. I'm gonna show you one of my favorite features about Directus Now and that I'm using the REST APIs.\u003C/p>\u003Cp>It also generates GraphQL APIs for you, but this is great. This is one of the things that I love. I could go in and tell it specifically what fields I want using the REST API, and I can even fetch so I'll just do products dot wildcard there. I could even go in and fetch products underscore ID the related data in a single call. So now you can see I've got our giveaway page data up here.\u003C/p>\u003Cp>I've got our products data here with all of our options and things like that, and I've only had to make a single call. Amazing. But we're gonna run out of time if we don't start building an app. So we've got our Nuxt app running at local host 3,000. Let's open this up and start trying to build something.\u003C/p>\u003Cp>Cool. So we'll go in to our pages, Let's create a new folder. Giveaways. Okay. Looks like I already did that.\u003C/p>\u003Cp>Not sure why I cheated a little bit there. But, we will do giveaways slash ID. So we use brackets here to set up a dynamic route inside Nuxt. We'll just do a basic component here, and now I just want to fetch that giveaway data so we can render it. Now typically, if I was using server side rendering or something like that, I would probably want to use the Nuxt async data calls.\u003C/p>\u003Cp>It's got a nice use async data as the composable that has a lot of nice little stuff built in, but I'm not super concerned with that here and honestly, this is gonna be a private link. So what we're gonna do is just call the giveaway. We're gonna go giveaway equals await use directus, that's my composable that wraps the API or, I'm sorry, the SDK. I'm gonna do read item, that's the giveaway syntax or the, I'm sorry, the SDK syntax, the giveaway syntax. We don't even know what that is.\u003C/p>\u003Cp>And then I need to pass it an item. So before I do that I'm gonna use route from Nuxt and then I can do something like this where we have route dot params dot ID And then the 3rd argument here is just an object with options. So in this case, I'm just gonna use fields for right now and show you what that looks like. Now on the front end, typically what I'll do a lot is just render that out so I could see what we're working with. Now how do I actually navigate to this?\u003C/p>\u003Cp>Inside Directus, I could go into my data model for our giveaway page, and maybe I wanna set up a button where I can actually get to this specific item. So we'll just create a new button, we'll say view giveaway, And, you know, if this is live, obviously, you're gonna change the URL. But this is gonna be my Nuxt URL, HTTP / localhost3000/giveaways/id, and then I can click this plus button to add the dynamic variables from the ID of this item. If you added the raw value, you can see we're just using mustache syntax here. We'll hit save, save, you give away.\u003C/p>\u003Cp>Looks good. We go to the giveaway. Now I've got this button I could click that I view giveaway and boom, should be showing me that, but instead I'm getting forbidden. Why are we getting forbidden? If I look, I could see that I left off the s.\u003C/p>\u003Cp>I'm assuming that's probably part of it. So let's just refresh, and boom. Now I can see our giveaway. So here's our actual data for this specific giveaway. It's great.\u003C/p>\u003Cp>So let's style this bad boy a bit. Trying to remember what I've actually got in here as far as like the layouts. So the Nuxt layout directory is super handy, this is the default layout, looks good. What are let's just copy what we've got on our index page here, and we'll go to giveaways. Alright.\u003C/p>\u003Cp>So I don't even know that we wanna flex all of this, do we? Let's give it a purple background, so we'll do pgprimary500 with full height full screen. What do we got? Okay. There we go.\u003C/p>\u003Cp>Alright. And then we'll give it a bit of padding. Now we'll do like a white background. I think that's kind of similar to what we've got here. Yeah.\u003C/p>\u003Cp>We got some funky business going on there, but we got PG white. Yeah, p 8 and then we've got, okay. Thank you GitHub Copilot for the assist. We got the giveaway dot title, giveaway dot description, that's not quite gonna cut it. So we've got the div here, That's actually HTML that we're getting back, so let's do the pros class in tailwind.\u003C/p>\u003Cp>There we go. GitHub Copilot is getting a lot better these days. Is it still is it totally there? I don't think so. Okay.\u003C/p>\u003Cp>So we appreciate you big time. So as a thank you, please select your color and reset. Maybe this is text center, I wanna center that up. What do we have here? Bgymaxwith3xlmxauto.\u003C/p>\u003Cp>We'll get that in the middle of the page. Why is our h one not rendering? Right? This should be a big bold font. It's not rendering, and if I actually took the time to figure out my types, it would be because that's not the actual key.\u003C/p>\u003Cp>It is headline. Alright. So we've got our direct to swag headline. Let's center that up as well. Cool.\u003C/p>\u003Cp>Alright. Let's add in our products. So if we open up our Vue dev tools, we go to ID, and I just look at the data that this actual component has, you can see we still have to fetch those products. So what I could do is come into the field section, we'll go to products, We will within that, we're gonna fetch the product underscore ID, and we'll get all the root level fields for that. Let's see what that looks like.\u003C/p>\u003Cp>Products. Why are you not giving me what I want? Of course we can always do this as well. Give away. Products is not coming through.\u003C/p>\u003Cp>Do we have permissions enabled for those products? Let's take a look. Giveaway products, people products. Looks like we've got read permissions set up for all of that. What am I doing wrong?\u003C/p>\u003Cp>Products. Products. ID. Is that it? Where are you?\u003C/p>\u003Cp>Okay. There we go. In my haystack, fat fingered, forgot to put an s in here. My wife makes a likes to make fun of my sausage fingers anyway. So alright.\u003C/p>\u003Cp>So now we got our products. We'll render these products out. V for product and giveaway dot products. Just show the product. Alright.\u003C/p>\u003Cp>So there's the individual products. Again, part of the rules are there are no rules, so let's hop into Tailwind UI. Again, one of my favorite tools. Just a nice library of components that they've already got kind of set up for use cases such as this. Looks like we could make use of this particular one.\u003C/p>\u003Cp>Maybe we don't want it served in a modal window though, so we just grab let's grab everything inside the dialog panel. And I'm not sure how the outcome is gonna go here. This is probably gonna break some stuff. Okay. So you can see we've got some products here.\u003C/p>\u003Cp>Nothing is actually working correctly because we don't have things like selected size, let's actually just skip this out and we'll bounce into a component. So let's call this the product card, we've got a v comp ts, got my little snippets set up and we'll just copy and paste that in there. So that's our templates, what do they have for the script section of this? Looks like they have selected color, and they've got some headless view. Luckily my Nox starter already has those set up and then they have a product.\u003C/p>\u003Cp>So I'll just copy this stuff down. Do we get something on this end? We're getting somewhere. Looking good, except none of this is actually rendering out. That's okay though.\u003C/p>\u003Cp>We're gonna sort through that. Right? So we don't need the reviews. We'll just chop those. Let's get this down to bare bones.\u003C/p>\u003Cp>Right? The color picker, so we need the size picker. Add to bag, we're probably not gonna need that. View full details, we're not gonna need that, but this is not updating, so I need to actually go in and just swap this out. Product card, b 4 product card.\u003C/p>\u003Cp>And then we'll probably end up doing something like this where we have key equals product dot ID. We probably wanna fetch that ID here as well. So do that. Is that right? No.\u003C/p>\u003Cp>We're gonna put it here. ID. Alright. So now we're rendering a product card. How we doing on time?\u003C/p>\u003Cp>We got 27 minutes left. We are over halfway here. We still gotta be able to submit this form. And then we also are gonna want to pass it, so our product is gonna be product dot products underscore ID. Alright.\u003C/p>\u003Cp>So we're not doing anything here now because I'm just using the default setup from Tailwind, from our Tailwind UI components. Where's that size guide coming at from as well? Let's get rid of that. Alright. So we just want the basic details, right?\u003C/p>\u003Cp>We're going to define some props, so we'll go prod const props equals define props. Product type is an object. Again, normally I would type this out with an interface or something like that inside using typescript, so we've got that, but, yeah, whatever. Alright. That should break this particular item.\u003C/p>\u003Cp>If I refresh, I'm probably gonna get a whole bunch of stuff going on. Got a product. Oh, let me just clear this out because now we no longer have product there. Okay. So we got the bunny tumbler, we've got some colors, we've got some stuff going on.\u003C/p>\u003Cp>Is this open and closed? We don't even need that. Alright. So this particular NUC starter and I think I'm gonna have to make this available online just in case anybody wants to look it up and decide if I was using steroids or not, but we'll do product dot images dot 0. Let's see where where that gets us.\u003C/p>\u003Cp>Not very far. Alright. So a Nuxt Image is already set up on this particular product or I'm sorry, this particular starter. Let me reload the page here and see what's happening. Alright.\u003C/p>\u003Cp>So we've got our products. We're passing that product down to the product card. And what are we actually getting for the images? We're just getting, not getting anything for the images. So, I could go back up to our giveaway.\u003C/p>\u003Cp>And now let's drill into our images, if I could stop making typos. Images. Boom. Let's see what we got now. Images and array of objects.\u003C/p>\u003Cp>At least now we have a direct us files ID that we can use to access that image. I would probably, you know, if you wanted to get, like, the title description, the alt text, stuff like that, we could drill in even further like this where we have the directus_files_id, and we get the ID, we get the title, get the description. Now let's look at what we've got. We just keep getting further and further into nesting. But, hey, super helpful anyway.\u003C/p>\u003Cp>Alright. So now we're getting that if we go to our product card, maybe we just use like a computer prop here. Constant image equals computed and then we'll do return product dot no. We're gonna do props dot product dot images dotzero.directusfiles.id dotid. Alright.\u003C/p>\u003Cp>And then here inside our Nuxt image I can just do this where we say image. Clear that alt text for now. Actually we could use the title. Right? So we have let's change that we're returning the actual object instead of the ID We'll do image dot ID, and then the alt text could be image dot title or description.\u003C/p>\u003Cp>Great. Same result. We've got our colors. We're at what? 22 minutes, does that need to be overflow hidden?\u003C/p>\u003Cp>Where's that coming from? That coming from our I think this was set up for like a, like an application instead of a scrolling page. Bg primary and hide screen, hide full. Yeah. Okay.\u003C/p>\u003Cp>Let's not stress over that for the moment. We've got bigger fish to fry. So, we've got our images, we've got our name, let's get our product description into this, product name, product price, the product description is gonna be again vhtml. So we'll just use the Tailwind Class Pros vhtmlproduct.description. Close that.\u003C/p>\u003Cp>Let's see what we got. This is the most amazing tumbler ever. There we go. Alright. So how do we get the size and and, like, our different options to display?\u003C/p>\u003Cp>Alright. If we look at our product card here, we've got our different options, for the tumbler there's just one option. So we are going to actually, do we need I don't think we need both of these. Right? We will just loop through and create each of those based on the individual options.\u003C/p>\u003Cp>So we've got a radio group, and this is the color. Let's just call it the options picker, and we get a v four product, let's call it option. Options. Yeah. That's fine.\u003C/p>\u003Cp>And product dot options, then the key is gonna be options.name. Okay. And then here we'll say options dot name, color, selected color, This will probably be something like selected options and that'll be a ref and object. We can do it either way. Let's see what we got.\u003C/p>\u003Cp>V model selected. Options and then we'll use the option dot name. We use the key for that. Choose a color, we don't really need that. In product dot colors, in v for value in no, let's do option in options dot options, maybe we call it option, option dot name, Option dot name, selected option, option dot options, Option choice.\u003C/p>\u003Cp>Option value. Naming surface fun. Right? Option value dot value. Okay.\u003C/p>\u003Cp>And then the value is what? Optionvalue.value.optionvalue.value. With the color dot selected color Not sure what that's doing there. Let's reload and see what's broken. So at least we're looping through these.\u003C/p>\u003Cp>Color dot name. Option dot name. Color.bgcolor. Why are these not actually displaying? Radio group, what are we passing into you?\u003C/p>\u003Cp>We are running out of time. V4 and product options. So we got a radio group. Let's just omit that. Missing something somewhere.\u003C/p>\u003Cp>Product card, let's look at we got our options, product dot options dot name option in options. So the option dot name and option dot values is what we need to loop over. Boom, boom, boom, And looks like I copied the wrong one. So we would probably be showing the values here if I go back to, let's copy this actual radio group option here. What's the difference?\u003C/p>\u003Cp>Div class. Just copy and paste this inside their size and stock. Don't need it. This is gonna be the option value dot value. This is gonna give us what we want.\u003C/p>\u003Cp>Oh, hey. Look at this. Now we're getting somewhere. Right? Looking beautiful.\u003C/p>\u003Cp>Not exactly beautiful, but definitely better than what we had. Right? So now let's take a look at our product card. If we look at our product, selected options dot ref, why is our ref not showing for those? Let me just refresh.\u003C/p>\u003Cp>The props, there's our setup, the selected options, this is for the first one, the color is white. Okay. Now if I select the other ones, we check the product card for that one. Where are you? Mister product card.\u003C/p>\u003Cp>Selected options are color, dark heather. Looks like we could probably work with this. Right? Now, yeah, we can get carried away here and do like a composable or something like that for our form. Let's just do like a watch and if we were gonna watch what selected options selected options dot, What is the actual watch syntax?\u003C/p>\u003Cp>Deep true. No. What is the view watch syntax? Alright. With the composition API, what are we looking for?\u003C/p>\u003Cp>We've got a function. So we're gonna watch selected options. Okay. And then we've got the functions. So we got the value, console log, maybe we want to omit that, so we'll pass that up, const emits equals define emit And let's just omit, updated.\u003C/p>\u003Cp>Sounds great. Alright. Selected options, we are going to omit. Updated. We will value true.\u003C/p>\u003Cp>Okay. Alright. So if we go back to our ID here, let's add a handler for this. Maybe we just do our order here that will be reactive. Okay, and then we've got our order products, we've got the order, what do we have?\u003C/p>\u003Cp>A customer? No. It was the person, is what we called it. Alright. That person has a first name, last name, email, and then we have an address.\u003C/p>\u003Cp>Great. Alright. So now we've got an order. Cost order equals react. What's the problem with this?\u003C/p>\u003Cp>Alright. Great. So now we'll probably need a handler. So let's do a function to handle that emit. So we'll do update cart or update products, products order dot products okay and then when update update products.\u003C/p>\u003Cp>Cool. Alright. So let's verify and see what's happening now. So I'll just refresh my page. Unable to fetch.\u003C/p>\u003Cp>Oh, what am I doing wrong? Hydration completed but contains a mismatch. What what are we doing wrong here? This is always fun. V if or if we just wrap this really quickly.\u003C/p>\u003Cp>Giveaway. Failed to fetch dynamically imported module. I understand. Active from you. So what's going on?\u003C/p>\u003Cp>Dying on the vine here. I probably should have been using the, Nuxt Async data all along, We could probably get around this maybe just by using turning SSR off for now, but obviously, not the best solution. Does not have provided a named export called select. What is this? Is this in the product card?\u003C/p>\u003Cp>Oh, I don't know why we imported that. How did that get imported? Okay. So it probably wasn't, any issue with the SSR there but, oh well. Alright.\u003C/p>\u003Cp>So we're back on our giveaway dot ID. Let's refresh just to make sure we've got everything. Where is our where's our app at? Okay. So we look for our ID.\u003C/p>\u003Cp>Now we've got our order object and hopefully, this should be updating, but it's not. Emit updated. That could be why. I actually have to spell it correctly. So we refresh, try again.\u003C/p>\u003Cp>If we look at our products, are they updating? They're still not updating. Wonder why that is. So console dot log, good old console dot log products section. Okay.\u003C/p>\u003Cp>Products. Okay. Okay. So what do we actually want to do here? This will be we actually need to pass the product ID, don't we?\u003C/p>\u003Cp>So updated. Let's do this where we have an event. We want to pass the product ID and the event. Alright. Update products.\u003C/p>\u003Cp>This will be what? Clock is ticking. Let's cheat a little bit. Update the porter products array with the selected product. Let's see what GitHub Copilot comes up with really quickly.\u003C/p>\u003Cp>It will look through the array, find the index for the item. Okay. Nobody said it was, was easy. Right? So now we've got our products, we've got our ID, we've got the options for the product.\u003C/p>\u003Cp>What do we want to, It's actually the product. Right? What is that gonna be inside the database? Let's just take a look at it inside our data model. We got the order, we got a person, Did we not add that reverse?\u003C/p>\u003Cp>We didn't add that reverse, so we we're gonna need to add that really quickly. Many to 1, 1 to many, boom boom. Products. Oh, actually, that's gonna be our items. Alright?\u003C/p>\u003Cp>It's gonna be the items on the order. Order items, order. Okay. So that's actually gonna change. That'll be our items.\u003C/p>\u003Cp>And then our order item itself has the item options, so that'll be item options. Item options equals event, and then the product. Order products dot index item options. We'll just see if this goes according to plan. Okay.\u003C/p>\u003Cp>Blah blah blah. We take a look, we've got our giveaway page. What am I missing? Order dot, oh, order dot products dot push. Forgot to update this.\u003C/p>\u003Cp>So this will actually be order dot items, order dot items. Okay. So now we've got our items for the order. There's our product, 123. Shouldn't have 3 items, should it?\u003C/p>\u003Cp>Maybe that was a mistake. Running into snags everywhere. Lots of fun. Alright, so how do we actually submit this? We still don't have our form either, do we?\u003C/p>\u003Cp>Let's build a form. So a form. And actually, I think there is the Nuxt UI component that is built into this thing. So let's just do a form. What are those?\u003C/p>\u003Cp>A u input, u form group I think is the right one. Label equals first name, V model equals order dot first name. Oh. That's gonna be you and put the model order first name. Let's see what we get.\u003C/p>\u003Cp>Okay, now we've got a form, Let's actually wrap that just a little bit. Max width 3 x l. Actually, let's just move it up inside that. Why why are we doing that? There we go.\u003C/p>\u003Cp>Alright, so we've now we've got our form inside here. Bgwhite, shadow. Okay, we got our first name. What do we have? We got a last name, got an email, street, city, GitHub Copilot for the win on this one, And zip.\u003C/p>\u003Cp>Alright. That's not looking very pretty, so let's actually set these on a grid. Grid calls 2. Alright. This will be class grid callspan 2.\u003C/p>\u003Cp>And give it some gap, gap 2, let's say 6, right? Too much, gap 4. We'll give the form itself some space in between and then we just need a button, submit. Let's make it block. Okay.\u003C/p>\u003Cp>Class equals call spin 2. Alright. So big giant form, now we need to submit the order, submit order, await, direct us, use orders, order, giveaway dot ID, create item, that looks correct. Let's wrap this in a try catch error console dot error. Alright.\u003C/p>\u003Cp>Time is a ticking. Let's see, we got 43 seconds. Let's see if we can actually get this done. Brian Gillespie, email bryantat directus.io. 123 Main Street, Bluefield, West Virginia.\u003C/p>\u003Cp>Submit. Oh, duh. Dummy. Come on now. Submit dot prevent.\u003C/p>\u003Cp>Oh, no. He's typo. Submit dot prevent. Submit order. One second.\u003C/p>\u003Cp>We hit the timer here right at the very end, just as we were getting really good. I just wanna submit this out and see if it's actually gonna work. Test city, 1232222. Submit. Did we get an error?\u003C/p>\u003Cp>Post local items orders equals forbidden. So I'm getting that error because there are permission settings that we didn't control order items. We forgot to set that up. But now if we were to go back and just try this again, orders forbidden. Yeah.\u003C/p>\u003Cp>So, something in the way that I'm passing the data here, don't have permissions to access this. It is probably something to do with the person data that I did not get updated. And it's actually what? State Street Zip. This is the this is the main issue why you shouldn't trust AI.\u003C/p>\u003Cp>In our hurry, this actually should have been order dot address dot street dot city dot state, all of that. Didn't even pick it up. Hopefully, you're not coding against the clock like I am, but that's one of the the issues there. Test, test, test, test, submit. Still getting some issues.\u003C/p>\u003Cp>So, we tried our best. I still think this looks pretty good. And with another 20 minutes or so, 10 minutes or so, we could have completed this out where it would actually submit the order to Directus. But I hope this is a good example of just how quickly you can build apps with Directus, and a a front end framework like Nuxt. Directus gives you a lot out of the box, which makes speed runs like this possible.\u003C/p>\u003Cp>This was a lot of fun. I don't think that I'm gonna be replacing swag.com anytime soon, but, nevertheless, if you had different needs or you wanted your own customized platform, you could quickly and easily build it with Directus and have something that was specific to your company. These platforms like swag.com and others are obviously, they have a lot going on on the fulfillment side. But with Directus you could build custom ecommerce like this, custom experiences for people rapidly. So that is it for this episode of 100 apps 100 hours.\u003C/p>\u003Cp>Thanks for sticking it out with me and I hope you'll tune in for the next episode. Ciao.\u003C/p>","Alright. Alright. Alright. Welcome back to the next episode of 100 Apps 100 Hours where we rebuild or build some of your favorite apps in 1 hour or less or die trying or publicly fail trying is probably more appropriate. I'm your host, Brian Gillespie, developer advocate at Directus. Welcome. Super excited to have you. Today, we are going to be building a gifting platform. Oh, Brian, that sounds really exciting. Actually, to me, it is. So let's dive into the details of this. I've recently gone through at Directus and set up a swag program. So we've got tons of users worldwide that use our software getting, merchandise like branded cups or mugs or t shirts, into the hands of our user base, incredibly difficult challenge when you consider that we've got users in the US, we've got users in Germany, France, Namibia, just every country that you can think of. We've got people that we would like to thank and show appreciation for. Very challenging. So inter gifting platforms. We are going through the process currently with a company called swag.com. They offer a shopping cart and e commerce experience where you can log in and, basically pick your swag, upload your designs, and place orders that they will then fulfill for you. There's quite a few of these types of companies like Sendoso, I found another one called SwagUp, and this one is Codus Design. It basically a swag platform where you place an order, they manage all the fulfillment and inventory and actually delivering those items to your clients or your customers or your potential leads, potential prospects. They handle all of that for you. The part that I was very interested in, and I'm not discounting the fulfillment and warehousing and all the other bits and bobs that are included in here. That's probably the really difficult stuff. But, our past ecommerce or past swag solution was a Shopify store. And when you're trying to send somebody a thank you gift, it feels a little weird to have them check out on a Shopify store and enter a coupon code to actually get that get that swag or get that gift that you're trying to send them. And one of the reasons why I really like these platforms is the ability to send out a thank you page that looks very similar to this. There's there's no checkout. They don't have to enter a credit card or a coupon code. I send them a unique link. They can pick their swag, and we get them to enter in their information like their first name, email, country, and address so we know where to ship it. It is a great experience for them, great experience for us, win all around. That's what we're gonna be building today. Let's dive in. Just a quick recap of the rules. There's 60 minutes to plan and build, so that's plan and build in 60 minutes no more no less, And aside from that there are new rules. We'll use whatever we have at our disposal whether that's AI or all sorts of other cheat codes from around the Internet. Let's get started. We're gonna dive in. Alright, so before I build any apps, I usually like to take at least 5 minutes to plan. These planning sessions are always fun because it's hard to test your assumptions to get feedback when you have 5 minutes to plan an app. But here is my initial functionality that we're gonna build into this app. We're gonna need to manage our products and product options. We will need to, create a giveaway page or several different things that the the companies that I looked at called this, like a redemption page. I think giveaway is probably a good one. Thank you page, create a giveaway, a redemption page. Place an order, place a request. Not sure it's a order because it's free, but hey, that's what we'll do. User places a request, k, let's say user can place an order request, and then we manage and fulfill those through the app. Alright. So that's the app that we're gonna build. There are 2 components. There's a back end. There's a single page on the front end. What are we gonna need as far as our data model? What is that gonna look like? So we'll jump in and let's do, we certainly have products. Oh, I'm not ready to drag arrows yet. We've got our products. What is next? We've got our giveaway page, right? Giveaway. But then we have customers, you know, we'd probably wanna track that as a separate table or those could just be people. And then we'll have orders and probably some actual order items. So when we look at this, all our products roll up to a giveaway and we this is probably gonna be like a mini to mini relationship because we could have many different products on many different giveaways. When we go to products those are gonna be to the order items, customer orders or items will be orders attached to giveaways. I I just really like drawing arrows half the time. So as far as the rough data model, this looks pretty good. You know, curious to hear your feedback if if you set up ecommerce before, especially on how you handle like product variants and things like that. But let's dive in and not waste too much more time. Let's actually start building this. I'm just gonna move this off to the side. What does our setup look like as far as how we're gonna build? I've got a local Nuxt starter app set up. I've got a Directus folder where I've got a Docker Compose file that just simply creates my Directus instance that we're gonna be using on the back end. And then this Nuxt application just has a SDK for Directus already pre configured with a plugin using the the Nuxt plugins and, you know, nothing too fancy. The only, like, major difference here is there's a composable, view composable included for making the Directus requests, because Nuxt has that nice auto import feature. Alright, so let's log into our Directus instance and start building something right away. Let me look at what I'd use for my password here, just paste that and as we look let me get my data model up in view so we can actually start building. But as we look, this is a blank instance, I'm not doing any kind of crazy cheating here. Let's start building. So we'll create a new collection for products, we'll track some dates on this, you know, is this a draft product or not? Maybe we don't really need that for this particular instance. Alright. So on our products, what do we have? What are we using? We've got a name for our products, that's great, that's just a string. We've got a description for our product, We probably have some images for our product. So we've got some images. This is going to be using the relational fields inside Directus. And what's happening behind the scenes here as I'm building these collections and adding fields to it, the Directus API is mirroring this setup inside my Postgres database. And at the end of this, as soon as I get done building this data model, I'll have a ready to go API that I can then call on my front end to render those products. So we've got name, description, images, we're probably gonna have some options for our products like color and size. A more robust setup here would be to create separate collections for those, you know, a a product options and then maybe a product options values so that that could be dynamic. To streamline this, I think I wanna go with the repeater inside Directus. So this is just a JSON store and we'll call this options. So our options, we're gonna edit the fields here. The first option is gonna be the name of the option and then that'll be a string. We require a value for that, that'll be an input. It's fine. Let's make it full width and then next let's add values. So we're gonna get a little meta here and then I am going to embed a repeater within a repeater. Sounds pretty wild. So the field here, let's call it value. And I could probably have like a label and value in case the the value that I wanted to track was different than the label, but this looks pretty okay. We'll hit Save. We've got name, we've got description, we've got images, you know, if I had something like a SKU we can work on that, but let's keep it simple, right, let's keep moving. What's next? We have our giveaway. So this is gonna be a giveaway page, and if I pull up one, like, the direct to sample, we've basically got a logo, we've got a headline, we've got some description, then we have the products and we have a form. So let's add a table for giveaways. We'll go in and we'll set a status on the giveaway if this is published or not. Let's go in, we'll create a new field, we'll call it headline. We'll just make that a string. Then we have a description that we could set. So we'll do a description. Again that will be rich text so we could potentially have, lists or, you know, bold italicized text. And then we want to add our products. Right? So with our products, we're gonna use the relationship features inside Directus, and we're going to use that many to many relationship. Because one giveaway could have many products, and we could use many products on many giveaways on we use one product on many giveaways. So the let's call this field products. Our related collection is also gonna be products, and we don't want to allow duplicates. We do wanna show a link to the item so we can go back to that product. And I'm just gonna open this in advanced mode. So behind the scenes, what's happening when we use this many to many relationship? Inside the Directus or inside our Postgres database, Directus is creating a junction table called giveaways_products. Now I could adjust what that table is called, and even the foreign keys that it's setting up inside that junction collection, what it's naming those. But I'm just gonna go with autofill in this case. If I want to add the reverse relationship back on the products, so if I wanna reference those giveaways on the products themselves, I can do that here as well. And then maybe we add a sort field just to control the order. Maybe I want the shirt to show up first before the other items. Great. Directus is gonna tell me what all it's going to create inside my database, and now we've got our products. What else do we need? Do we need anything else here? The form, maybe we hard code that. Great. So if we look what else? We've got a customer, we've got orders, and order items. Alright. So let's go ahead and set those up as well. So we got a customer, actually let's just call it persons, people. Naming stuff is always the hardest thing. This may not be a customer. Right? So let's just go with the more semantic name and call it people. So people have a first name, they have a last name, they have an email address, and are we what else are we capturing? Capturing an address for those people. I'm assuming that could be on the order itself. Let's just roll with this. Right? First name, last name. Maybe we'll capture the address on the order. We could store it here as well, but we'll keep it simple. So we've got then we have an order and the orders are going to what? What are we gonna have on the order? The order is going to have a customer or a person, where we who's the person attached to that order, that's gonna be a many to one relationship. Alright. So we got the people, person, people, are already regretting this decision. No turning back now. Right? And then on the reverse side of that, on our people, we'll show the orders. People orders within the data model. Great. Alright. So the person we have, an address, let's just store that as JSON data for now. So we'll do an address and what else? We have a status is approved, maybe. This is approved, so we want approval before we ship all of these out. Let's add that to the top. And then last but not least, we want to create the actual items involved with this specific order. So we've got an order items table or you could call it orders items. Again, we get into splitting hairs here. What else do we need? We probably do need a sort for those blah blah blah. Alright, so we got our order, we got our order items. The items are gonna have a link back to the product. So we've got a related collection is gonna be the products. So when we put those items in the cart, the order item will have a product, so that'll be a relationship and then let's just have a JSON field to store our product options. Maybe we call it item options just to keep them separated. Good. And then there is a 1 or many to 1 relationship. So there are many items to one order, back to that order. So we've got the order, the related collection is orders. Tell me I didn't. I did, didn't I? So this is a boo boo. I've got orders here. It should be or I've got order should be orders. That should be a a fun little thing to work around. Now, I could go into the database itself and edit that if I wanted to, but, no worries. We'll just roll with what we got. There's an order. It's a order. Sometimes you goof up. What do you do? You keep rolling. Alright. So we've got our data model. Right? Looks great. Let's go in and maybe add a giveaway. And we can just duplicate this one, right? Directus Swag. Still learning Arc. I like it as a browser. Yeah. If anybody out there is a Arc expert, maybe you could give me a couple lessons like a tutorial session or something. Alright, so we got our headline, we've got a description. Let's add a product, we don't have any products at this point, so let's create a product. We've got the Directus, let's let's call it the Bunny Tumbler 20 ounce, Great. This is the most amazing tumbler ever. Great. Now we want to add some images to this. So, I can actually import images from a URL inside Directus, which is super nice, especially for speed runs like this. I just click upload a file, click the little link that will import our file. Looks great. For our options, we've got color and for the value, we've only got one. It's just white. We're gonna offer that in white. Great. Okay. Let's go in and add one more product. We've got our Directus logo tee. Kinda sounded like goatee, didn't it? This is the softest t shirt you'll ever wear, and again, we'll just go in and let's copy the image address and you can see we've got a couple options here. We've got extra small through 2 x, so we'll add those in there as well. Just upload our file. Great. For our options, we've got color, which is a dark heather. I believe I should know, I'm the one who ordered this stuff. And then let's add another option for what size. Alright. So we got extra small, we got small, we got medium. It'd be really handy if we could just do this in line, wouldn't it? I'm sure there's a way. Where there's a will, there's a way. Alright. So we've got some extra small through 2 x. We've got our products. We've added them to a giveaway. Let's go ahead and publish this giveaway and hit save. Great. So we've got our giveaway, and let me just prove it to you. Let's open up Directus. Alright. So this is our actual database here. We can see we've got order. We've got some directus tables that directus has created for us, and then we have giveaways. So there's our data. Directus prefixes all the metadata tables that it so it keeps the SQL database pure. That way, you know, you're not locked in or anything like that if you decide to migrate away. Very nice. Very nice. So now let's actually take a look at our giveaway. Right? How do we fetch this on the front end? And Directus gives us these REST APIs out of the gate. So if we go to Directus URL, which is local host 805 5 items giveaways, we get no permissions. So I have to go back and by default, Directus controls locks down all of my data. It wants us to be secure. So we've got 2 initial roles in an account. We've got a public role, which controls what data is accessible without authentication. And then we have the administrator role, which has unrestricted access. Now if I was gonna put this behind a login, I would probably do something like creating a user role that didn't have access or didn't have admin access, but they would log in on our front end. In this case, I'm just going to use the public role. We are going to allow people to see the giveaways, we're gonna allow them to view the giveaways, they can create orders and order items, they'll need to be able to create people, and we could also have, like, a server token, where we're doing some of this on the the server side as well. But we're not super concerned with security on this one because we've only got 40 minutes left to build this app. So we will allow access to see the products, see all the product files, and then we're going to go into the Directus Files under the System Collections and we're gonna allow that. So now if I go back, I open this up go to 8055/items/ giveaways. Boom. There's our giveaway data. Looks great. And we don't see any of our products. I'm gonna show you one of my favorite features about Directus Now and that I'm using the REST APIs. It also generates GraphQL APIs for you, but this is great. This is one of the things that I love. I could go in and tell it specifically what fields I want using the REST API, and I can even fetch so I'll just do products dot wildcard there. I could even go in and fetch products underscore ID the related data in a single call. So now you can see I've got our giveaway page data up here. I've got our products data here with all of our options and things like that, and I've only had to make a single call. Amazing. But we're gonna run out of time if we don't start building an app. So we've got our Nuxt app running at local host 3,000. Let's open this up and start trying to build something. Cool. So we'll go in to our pages, Let's create a new folder. Giveaways. Okay. Looks like I already did that. Not sure why I cheated a little bit there. But, we will do giveaways slash ID. So we use brackets here to set up a dynamic route inside Nuxt. We'll just do a basic component here, and now I just want to fetch that giveaway data so we can render it. Now typically, if I was using server side rendering or something like that, I would probably want to use the Nuxt async data calls. It's got a nice use async data as the composable that has a lot of nice little stuff built in, but I'm not super concerned with that here and honestly, this is gonna be a private link. So what we're gonna do is just call the giveaway. We're gonna go giveaway equals await use directus, that's my composable that wraps the API or, I'm sorry, the SDK. I'm gonna do read item, that's the giveaway syntax or the, I'm sorry, the SDK syntax, the giveaway syntax. We don't even know what that is. And then I need to pass it an item. So before I do that I'm gonna use route from Nuxt and then I can do something like this where we have route dot params dot ID And then the 3rd argument here is just an object with options. So in this case, I'm just gonna use fields for right now and show you what that looks like. Now on the front end, typically what I'll do a lot is just render that out so I could see what we're working with. Now how do I actually navigate to this? Inside Directus, I could go into my data model for our giveaway page, and maybe I wanna set up a button where I can actually get to this specific item. So we'll just create a new button, we'll say view giveaway, And, you know, if this is live, obviously, you're gonna change the URL. But this is gonna be my Nuxt URL, HTTP / localhost3000/giveaways/id, and then I can click this plus button to add the dynamic variables from the ID of this item. If you added the raw value, you can see we're just using mustache syntax here. We'll hit save, save, you give away. Looks good. We go to the giveaway. Now I've got this button I could click that I view giveaway and boom, should be showing me that, but instead I'm getting forbidden. Why are we getting forbidden? If I look, I could see that I left off the s. I'm assuming that's probably part of it. So let's just refresh, and boom. Now I can see our giveaway. So here's our actual data for this specific giveaway. It's great. So let's style this bad boy a bit. Trying to remember what I've actually got in here as far as like the layouts. So the Nuxt layout directory is super handy, this is the default layout, looks good. What are let's just copy what we've got on our index page here, and we'll go to giveaways. Alright. So I don't even know that we wanna flex all of this, do we? Let's give it a purple background, so we'll do pgprimary500 with full height full screen. What do we got? Okay. There we go. Alright. And then we'll give it a bit of padding. Now we'll do like a white background. I think that's kind of similar to what we've got here. Yeah. We got some funky business going on there, but we got PG white. Yeah, p 8 and then we've got, okay. Thank you GitHub Copilot for the assist. We got the giveaway dot title, giveaway dot description, that's not quite gonna cut it. So we've got the div here, That's actually HTML that we're getting back, so let's do the pros class in tailwind. There we go. GitHub Copilot is getting a lot better these days. Is it still is it totally there? I don't think so. Okay. So we appreciate you big time. So as a thank you, please select your color and reset. Maybe this is text center, I wanna center that up. What do we have here? Bgymaxwith3xlmxauto. We'll get that in the middle of the page. Why is our h one not rendering? Right? This should be a big bold font. It's not rendering, and if I actually took the time to figure out my types, it would be because that's not the actual key. It is headline. Alright. So we've got our direct to swag headline. Let's center that up as well. Cool. Alright. Let's add in our products. So if we open up our Vue dev tools, we go to ID, and I just look at the data that this actual component has, you can see we still have to fetch those products. So what I could do is come into the field section, we'll go to products, We will within that, we're gonna fetch the product underscore ID, and we'll get all the root level fields for that. Let's see what that looks like. Products. Why are you not giving me what I want? Of course we can always do this as well. Give away. Products is not coming through. Do we have permissions enabled for those products? Let's take a look. Giveaway products, people products. Looks like we've got read permissions set up for all of that. What am I doing wrong? Products. Products. ID. Is that it? Where are you? Okay. There we go. In my haystack, fat fingered, forgot to put an s in here. My wife makes a likes to make fun of my sausage fingers anyway. So alright. So now we got our products. We'll render these products out. V for product and giveaway dot products. Just show the product. Alright. So there's the individual products. Again, part of the rules are there are no rules, so let's hop into Tailwind UI. Again, one of my favorite tools. Just a nice library of components that they've already got kind of set up for use cases such as this. Looks like we could make use of this particular one. Maybe we don't want it served in a modal window though, so we just grab let's grab everything inside the dialog panel. And I'm not sure how the outcome is gonna go here. This is probably gonna break some stuff. Okay. So you can see we've got some products here. Nothing is actually working correctly because we don't have things like selected size, let's actually just skip this out and we'll bounce into a component. So let's call this the product card, we've got a v comp ts, got my little snippets set up and we'll just copy and paste that in there. So that's our templates, what do they have for the script section of this? Looks like they have selected color, and they've got some headless view. Luckily my Nox starter already has those set up and then they have a product. So I'll just copy this stuff down. Do we get something on this end? We're getting somewhere. Looking good, except none of this is actually rendering out. That's okay though. We're gonna sort through that. Right? So we don't need the reviews. We'll just chop those. Let's get this down to bare bones. Right? The color picker, so we need the size picker. Add to bag, we're probably not gonna need that. View full details, we're not gonna need that, but this is not updating, so I need to actually go in and just swap this out. Product card, b 4 product card. And then we'll probably end up doing something like this where we have key equals product dot ID. We probably wanna fetch that ID here as well. So do that. Is that right? No. We're gonna put it here. ID. Alright. So now we're rendering a product card. How we doing on time? We got 27 minutes left. We are over halfway here. We still gotta be able to submit this form. And then we also are gonna want to pass it, so our product is gonna be product dot products underscore ID. Alright. So we're not doing anything here now because I'm just using the default setup from Tailwind, from our Tailwind UI components. Where's that size guide coming at from as well? Let's get rid of that. Alright. So we just want the basic details, right? We're going to define some props, so we'll go prod const props equals define props. Product type is an object. Again, normally I would type this out with an interface or something like that inside using typescript, so we've got that, but, yeah, whatever. Alright. That should break this particular item. If I refresh, I'm probably gonna get a whole bunch of stuff going on. Got a product. Oh, let me just clear this out because now we no longer have product there. Okay. So we got the bunny tumbler, we've got some colors, we've got some stuff going on. Is this open and closed? We don't even need that. Alright. So this particular NUC starter and I think I'm gonna have to make this available online just in case anybody wants to look it up and decide if I was using steroids or not, but we'll do product dot images dot 0. Let's see where where that gets us. Not very far. Alright. So a Nuxt Image is already set up on this particular product or I'm sorry, this particular starter. Let me reload the page here and see what's happening. Alright. So we've got our products. We're passing that product down to the product card. And what are we actually getting for the images? We're just getting, not getting anything for the images. So, I could go back up to our giveaway. And now let's drill into our images, if I could stop making typos. Images. Boom. Let's see what we got now. Images and array of objects. At least now we have a direct us files ID that we can use to access that image. I would probably, you know, if you wanted to get, like, the title description, the alt text, stuff like that, we could drill in even further like this where we have the directus_files_id, and we get the ID, we get the title, get the description. Now let's look at what we've got. We just keep getting further and further into nesting. But, hey, super helpful anyway. Alright. So now we're getting that if we go to our product card, maybe we just use like a computer prop here. Constant image equals computed and then we'll do return product dot no. We're gonna do props dot product dot images dotzero.directusfiles.id dotid. Alright. And then here inside our Nuxt image I can just do this where we say image. Clear that alt text for now. Actually we could use the title. Right? So we have let's change that we're returning the actual object instead of the ID We'll do image dot ID, and then the alt text could be image dot title or description. Great. Same result. We've got our colors. We're at what? 22 minutes, does that need to be overflow hidden? Where's that coming from? That coming from our I think this was set up for like a, like an application instead of a scrolling page. Bg primary and hide screen, hide full. Yeah. Okay. Let's not stress over that for the moment. We've got bigger fish to fry. So, we've got our images, we've got our name, let's get our product description into this, product name, product price, the product description is gonna be again vhtml. So we'll just use the Tailwind Class Pros vhtmlproduct.description. Close that. Let's see what we got. This is the most amazing tumbler ever. There we go. Alright. So how do we get the size and and, like, our different options to display? Alright. If we look at our product card here, we've got our different options, for the tumbler there's just one option. So we are going to actually, do we need I don't think we need both of these. Right? We will just loop through and create each of those based on the individual options. So we've got a radio group, and this is the color. Let's just call it the options picker, and we get a v four product, let's call it option. Options. Yeah. That's fine. And product dot options, then the key is gonna be options.name. Okay. And then here we'll say options dot name, color, selected color, This will probably be something like selected options and that'll be a ref and object. We can do it either way. Let's see what we got. V model selected. Options and then we'll use the option dot name. We use the key for that. Choose a color, we don't really need that. In product dot colors, in v for value in no, let's do option in options dot options, maybe we call it option, option dot name, Option dot name, selected option, option dot options, Option choice. Option value. Naming surface fun. Right? Option value dot value. Okay. And then the value is what? Optionvalue.value.optionvalue.value. With the color dot selected color Not sure what that's doing there. Let's reload and see what's broken. So at least we're looping through these. Color dot name. Option dot name. Color.bgcolor. Why are these not actually displaying? Radio group, what are we passing into you? We are running out of time. V4 and product options. So we got a radio group. Let's just omit that. Missing something somewhere. Product card, let's look at we got our options, product dot options dot name option in options. So the option dot name and option dot values is what we need to loop over. Boom, boom, boom, And looks like I copied the wrong one. So we would probably be showing the values here if I go back to, let's copy this actual radio group option here. What's the difference? Div class. Just copy and paste this inside their size and stock. Don't need it. This is gonna be the option value dot value. This is gonna give us what we want. Oh, hey. Look at this. Now we're getting somewhere. Right? Looking beautiful. Not exactly beautiful, but definitely better than what we had. Right? So now let's take a look at our product card. If we look at our product, selected options dot ref, why is our ref not showing for those? Let me just refresh. The props, there's our setup, the selected options, this is for the first one, the color is white. Okay. Now if I select the other ones, we check the product card for that one. Where are you? Mister product card. Selected options are color, dark heather. Looks like we could probably work with this. Right? Now, yeah, we can get carried away here and do like a composable or something like that for our form. Let's just do like a watch and if we were gonna watch what selected options selected options dot, What is the actual watch syntax? Deep true. No. What is the view watch syntax? Alright. With the composition API, what are we looking for? We've got a function. So we're gonna watch selected options. Okay. And then we've got the functions. So we got the value, console log, maybe we want to omit that, so we'll pass that up, const emits equals define emit And let's just omit, updated. Sounds great. Alright. Selected options, we are going to omit. Updated. We will value true. Okay. Alright. So if we go back to our ID here, let's add a handler for this. Maybe we just do our order here that will be reactive. Okay, and then we've got our order products, we've got the order, what do we have? A customer? No. It was the person, is what we called it. Alright. That person has a first name, last name, email, and then we have an address. Great. Alright. So now we've got an order. Cost order equals react. What's the problem with this? Alright. Great. So now we'll probably need a handler. So let's do a function to handle that emit. So we'll do update cart or update products, products order dot products okay and then when update update products. Cool. Alright. So let's verify and see what's happening now. So I'll just refresh my page. Unable to fetch. Oh, what am I doing wrong? Hydration completed but contains a mismatch. What what are we doing wrong here? This is always fun. V if or if we just wrap this really quickly. Giveaway. Failed to fetch dynamically imported module. I understand. Active from you. So what's going on? Dying on the vine here. I probably should have been using the, Nuxt Async data all along, We could probably get around this maybe just by using turning SSR off for now, but obviously, not the best solution. Does not have provided a named export called select. What is this? Is this in the product card? Oh, I don't know why we imported that. How did that get imported? Okay. So it probably wasn't, any issue with the SSR there but, oh well. Alright. So we're back on our giveaway dot ID. Let's refresh just to make sure we've got everything. Where is our where's our app at? Okay. So we look for our ID. Now we've got our order object and hopefully, this should be updating, but it's not. Emit updated. That could be why. I actually have to spell it correctly. So we refresh, try again. If we look at our products, are they updating? They're still not updating. Wonder why that is. So console dot log, good old console dot log products section. Okay. Products. Okay. Okay. So what do we actually want to do here? This will be we actually need to pass the product ID, don't we? So updated. Let's do this where we have an event. We want to pass the product ID and the event. Alright. Update products. This will be what? Clock is ticking. Let's cheat a little bit. Update the porter products array with the selected product. Let's see what GitHub Copilot comes up with really quickly. It will look through the array, find the index for the item. Okay. Nobody said it was, was easy. Right? So now we've got our products, we've got our ID, we've got the options for the product. What do we want to, It's actually the product. Right? What is that gonna be inside the database? Let's just take a look at it inside our data model. We got the order, we got a person, Did we not add that reverse? We didn't add that reverse, so we we're gonna need to add that really quickly. Many to 1, 1 to many, boom boom. Products. Oh, actually, that's gonna be our items. Alright? It's gonna be the items on the order. Order items, order. Okay. So that's actually gonna change. That'll be our items. And then our order item itself has the item options, so that'll be item options. Item options equals event, and then the product. Order products dot index item options. We'll just see if this goes according to plan. Okay. Blah blah blah. We take a look, we've got our giveaway page. What am I missing? Order dot, oh, order dot products dot push. Forgot to update this. So this will actually be order dot items, order dot items. Okay. So now we've got our items for the order. There's our product, 123. Shouldn't have 3 items, should it? Maybe that was a mistake. Running into snags everywhere. Lots of fun. Alright, so how do we actually submit this? We still don't have our form either, do we? Let's build a form. So a form. And actually, I think there is the Nuxt UI component that is built into this thing. So let's just do a form. What are those? A u input, u form group I think is the right one. Label equals first name, V model equals order dot first name. Oh. That's gonna be you and put the model order first name. Let's see what we get. Okay, now we've got a form, Let's actually wrap that just a little bit. Max width 3 x l. Actually, let's just move it up inside that. Why why are we doing that? There we go. Alright, so we've now we've got our form inside here. Bgwhite, shadow. Okay, we got our first name. What do we have? We got a last name, got an email, street, city, GitHub Copilot for the win on this one, And zip. Alright. That's not looking very pretty, so let's actually set these on a grid. Grid calls 2. Alright. This will be class grid callspan 2. And give it some gap, gap 2, let's say 6, right? Too much, gap 4. We'll give the form itself some space in between and then we just need a button, submit. Let's make it block. Okay. Class equals call spin 2. Alright. So big giant form, now we need to submit the order, submit order, await, direct us, use orders, order, giveaway dot ID, create item, that looks correct. Let's wrap this in a try catch error console dot error. Alright. Time is a ticking. Let's see, we got 43 seconds. Let's see if we can actually get this done. Brian Gillespie, email bryantat directus.io. 123 Main Street, Bluefield, West Virginia. Submit. Oh, duh. Dummy. Come on now. Submit dot prevent. Oh, no. He's typo. Submit dot prevent. Submit order. One second. We hit the timer here right at the very end, just as we were getting really good. I just wanna submit this out and see if it's actually gonna work. Test city, 1232222. Submit. Did we get an error? Post local items orders equals forbidden. So I'm getting that error because there are permission settings that we didn't control order items. We forgot to set that up. But now if we were to go back and just try this again, orders forbidden. Yeah. So, something in the way that I'm passing the data here, don't have permissions to access this. It is probably something to do with the person data that I did not get updated. And it's actually what? State Street Zip. This is the this is the main issue why you shouldn't trust AI. In our hurry, this actually should have been order dot address dot street dot city dot state, all of that. Didn't even pick it up. Hopefully, you're not coding against the clock like I am, but that's one of the the issues there. Test, test, test, test, submit. Still getting some issues. So, we tried our best. I still think this looks pretty good. And with another 20 minutes or so, 10 minutes or so, we could have completed this out where it would actually submit the order to Directus. But I hope this is a good example of just how quickly you can build apps with Directus, and a a front end framework like Nuxt. Directus gives you a lot out of the box, which makes speed runs like this possible. This was a lot of fun. I don't think that I'm gonna be replacing swag.com anytime soon, but, nevertheless, if you had different needs or you wanted your own customized platform, you could quickly and easily build it with Directus and have something that was specific to your company. These platforms like swag.com and others are obviously, they have a lot going on on the fulfillment side. But with Directus you could build custom ecommerce like this, custom experiences for people rapidly. So that is it for this episode of 100 apps 100 hours. Thanks for sticking it out with me and I hope you'll tune in for the next episode. Ciao.",[242],"78258a11-7e08-4e1f-8d3c-ca90c4275d73",[],{"id":172,"number":131,"show":122,"year":173,"episodes":245},[175,176,177,178,179,180,181,182,183,184,185],{"id":178,"slug":247,"vimeo_id":248,"description":249,"tile":250,"length":219,"resources":8,"people":251,"episode_number":135,"published":253,"title":254,"video_transcript_html":255,"video_transcript_text":256,"content":8,"seo":8,"status":130,"episode_people":257,"recommendations":259,"season":260},"saas-app","894439990","Building multi-user is a common convention when building Software as a Service. Bryant has an hour to build a todo app which manages users, teams, and ensures data can only be accessed within a team.","2c1fb560-c144-43ac-9e87-00e712be818e",[252],{"name":199,"url":200},"2024-01-01","Mission: SaaS App","\u003Cp>Speaker 0: Welcome back to the next episode of 100 apps, 100 hours where we rebuild or recreate some of your favorite apps in 60 minutes or less or publicly fail trying. Sounds really scary when we say it that way. I am your host, Brian Gillespie, developer advocate at Directus, and today, we're gonna be building a multi tenant SaaS application. I realize that is not everybody's most favorite app, but it is the architecture that powers a lot of your favorite apps, and it's a question that comes up a lot within our community. So by multi tenant, what do we actually mean when we say that?\u003C/p>\n\u003Cp>It means that we have multiple different accounts or different tenants in this case, so we serve multiple different groups of users in one single application. And all of their data is separated. It's, you know, team a doesn't see the data or can't interact with the data for team b. It is totally doable on our back end as we're gonna see, and it's a popular convention for a lot of the SaaS apps that you will use so that we could share resources like a database, you know, an actual front end application without having to, you know, commission a brand new database for every single account that we have. Alright.\u003C/p>\n\u003Cp>Sounds great, sounds interesting, something that we want to build. How do we build it? What are the rules here? If you're new to the series, there are only 2 rules. We have 60 minutes to plan and build an application, no more, no less.\u003C/p>\n\u003Cp>And I do like to include a little planning ahead of time so we know, or before we actually build, not ahead of time. We're starting fresh here. You've got as much of a clean slate as I do. And then the second rule is use whatever you have at your disposal, whether that's Tailwind, online resources, AI, anything is fair game. We've got to get as far as we can in 60 minutes or less or look like an idiot.\u003C/p>\n\u003Cp>So sounds great. Everybody knows the rules. We're gonna be building the multi tenant SaaS app. Let's open this up and fire off the countdown. Here we go.\u003C/p>\n\u003Cp>So before we actually touch any code or any data models or, you know, anything in our back end, let's go through and and plan this out a little bit. And usually I like to start with our functionality. So let's shrink this down a little bit. And what functionality are we looking for out of this application? We want to create and manage tenants.\u003C/p>\n\u003Cp>In this case, let's call it, Teams is a pretty good convention. You know, accounts would be one that I see often organizations and in some applications. We wanna create and manage users within those teams. We wanna make sure that all data, make sure all data is scoped to each team. Right?\u003C/p>\n\u003Cp>So team a can access team b's data and vice versa to the team. And then, you know, a SaaS app needs to do something, so let's do just like the standard to dos. So we wanna create and manage to dos. Perfect. Alright.\u003C/p>\n\u003Cp>So there's our functionality. Nothing particularly fancy but it is tricky to set up some of this, especially around permissions. You know, that is a a big piece of the puzzle. Security is always very important. So, you know, how how do we set this up and how do we do it within an hour?\u003C/p>\n\u003Cp>Let's go through and now let's let's work on our data model a little bit. So we got our data model. And what are we gonna need as far as our tables or our different collections that we're gonna be using? So we will have, what, teams. Let's shrink this up a little bit.\u003C/p>\n\u003Cp>We will have users, we'll have to do's, and you know, we may not get to this but if I was gonna actually have this be built as a SaaS that I could charge people for, we'd probably have something like subscriptions. And we didn't include that in the functionality because I'm not sure exactly how far we can get in an hour, but we'll get to as much as possible. Right? So in this scenario, a user could be part of one team or, technically I guess we could build it so that a user can be in multiple teams. Multiple teams.\u003C/p>\n\u003Cp>Let's break that in half so we can actually see that. To do's belong to a team. To do's belong to a team assigned to a user. And then there's probably a a junction table here that is what? Users and teams.\u003C/p>\n\u003Cp>Oh, let's use something that's not connected. Right? So this is probably a table called something like teams users because it is a many to many relationship and just to get fancy and finish out our little arrows could look something like this. I'm definitely not an artist but you get the picture. Alright.\u003C/p>\n\u003Cp>So we've got teams, we've got users, we have to do's, we have team underscore users or or some of junction collection. We potentially have, subscriptions. So what do we have set up to actually build this? I'm using Directus for the back end of the application. I've got a super simple Nuxt application for the front end that we may or may not get to that and I've just got a little boiler plate that, uses the Directus SDK for that information.\u003C/p>\n\u003Cp>Let's jump right in. So if I pull up my Docker Compose and that's how I've got Directus set up locally, just through a simple Docker Compose file that you pull down from the documentation. And I'm gonna get logged in to our first user, and I'll just copy and paste this password here because I suck at typing a lot of times. Alright. So you can see that we've got a blank application.\u003C/p>\n\u003Cp>There is no data model. We've got a single admin user. We're starting from a clean slate. So if I just pull this up and we rearrange all these windows, maybe let's pull this over to the side so we can actually see what we're building, And I'll zoom in a little bit so you've got a better view. Maybe we don't put the clock in our face, so we're so we're not beating up against the clock.\u003C/p>\n\u003Cp>But how do we start? You know, let's start by creating this tenant collection. I'm gonna call this teams. You could easily call it tenants. You probably want to whatever makes the most sense in the context of your application is probably what I would use.\u003C/p>\n\u003Cp>Right? So the teams here, we're gonna generate a primary key called ID. We're just gonna use a UUID for that. As far as the status of that, yeah, we probably want a status for the team. We don't need a sort field.\u003C/p>\n\u003Cp>And then these others are just system fields that you can create, to give you a little shortcut. You know, when was it created? What was the user that created it? This specific team, blah blah blah. Alright.\u003C/p>\n\u003Cp>So as far as the team, what are we gonna need for the team information? We'll probably need a name for that team. That's great. Maybe we want to have a slug for the team. Could be a a good one to have.\u003C/p>\n\u003Cp>That way, we're not using a weird UUID, but instead a slug for the team. And let's go in and, adjust this a little bit. We're going to look for the interface section and Directus has the slugify option to make sure that slug input is URL safe. And one of the other things I'm gonna do here is just open up my little mouse pose tool. If you guys are ever wondering how I do this little highlight on the screen, that is a tool called mouse pose, very handy piece of kit.\u003C/p>\n\u003Cp>Alright so we've got our teams, now let's add our users, right? I could create a separate user collection but when I fire up Directus, it gives me a system collection for users that I can use to actually I could just reuse that for all of the authentication, all of the roles and permissions because what happens behind the scenes with Directus as my back end is it will mirror all these tables or these collections that we're creating inside the app to MySQL database, which is super handy. I I keep MySQL pure and, MySQL databases remains pure. And I've got built in REST and GraphQL APIs that are also automatically generated based on this data model. So tremendously powerful, enables you to build really quickly.\u003C/p>\n\u003Cp>So we're just gonna use that that system collection. I'm gonna I am gonna go in and create a new one. Let's just call it team users, and we'll generate a UID for that. We'll leave that one blank for now. Let's go in and add our to do's.\u003C/p>\n\u003Cp>So each to do we'll need, who's the date created, who was the user created it. We wanna give those a status as well, maybe a sort, so we can rank those in order of priority. So we've got the bare bones of our data model here, and we've got the again, we're gonna use the directus users system collection. How do we go and actually start setting up the tenancy? Right?\u003C/p>\n\u003Cp>Because right now, if anyone were to log in to this application, they would see all the data that we have available. So first, we are going to go in and create our relationships between these different collections, or which are just equivalent to tables within your Postgres database, let's first go through and do the teams and users. So a user can be in multiple teams. Let's go in and model that relationship. So we'll hit create field.\u003C/p>\n\u003Cp>We're gonna look for the many to many relationship inside Directus here, and let's just call this our users. Alright so the related collection is going to be users as well, actually it's not. It's gonna be directus_users, and that'll turn purple letting me know that that is an actual collection. And then we probably wanna show a link to the item. But instead of hitting save here, I'm going to hit continue in advanced field mode or advanced mode because I want to control what my Junction collection is named.\u003C/p>\n\u003Cp>So I'll just go in, we've already got this, it's called team underscore users, And I could go in and edit the, the foreign key fields here as well. It's just gonna be teams underscore ID, directus underscore users underscore ID. That's fine. Not a big deal. And if I wanted to, I can also add the reverse or the inverse relationship.\u003C/p>\n\u003Cp>Somebody will correct me on the comments. But, I can add that back to the Directus users collection so that I can reference that in any of our permissions that we're going to use later. We probably don't necessarily need a sort field, not a big deal. And the rest of this is pretty standard. So let's go ahead and save that.\u003C/p>\n\u003Cp>And now we have our users that are linked to, that direct us users table. So if I were to go in and create a team, I can see that I can add existing users or create new users for that team. Great. Alright. So next, let's take a look at teams and to dos.\u003C/p>\n\u003Cp>Right? So the to dos belong to a single team and I can assign those to dos to a user. So if we flesh out this data model a little bit more, we probably got a title or a name for this to do, let's use that. We probably have a description, so for that we'll just use our WYSIWYG editor inside the back end here. And I can even control what options we have as far as HTML content, but we'll just keep this the standard one for now.\u003C/p>\n\u003Cp>Alright. And then we've probably got a user that this has been assigned to. So we're gonna scroll down to our relational section. And depending on how we wanted to assign these, if we wanted to assign this task to or this to do to many users, you know, I would use either like a many to 1 or a one to many. In this case I just want to assign it to a single user.\u003C/p>\n\u003Cp>You know, whoever we assign this to is ultimately responsible for getting this thing done, and we're just gonna call this assign to. So we're going to use the many to one relationship within Directus, call it the assign to, and our related collection is going to be Directus users. So we'll hit save, that's who we assign this to. And now, let's go in and make sure this is applied to a team. So we need that relationship to a team so we can actually filter that out.\u003C/p>\n\u003Cp>Right? If team a is logged in or a user on team a is logged in, we don't want them to be able to see or interact with any of the to dos for team b, unless they are part of that team, which makes it a little more complicated to model as well. But we'll just call this, team or, you know, it could be team ID. Whatever. I'm gonna I'm gonna stick with team, and then the related collection is teams.\u003C/p>\n\u003Cp>Straightforward enough. Right? So if I were to go in now and we create a to do actually, let's just first create our team. Right? I've been talking about team a, there's the slug for team a, and team b.\u003C/p>\n\u003Cp>Cool. So now we have 2 teams, And I can go in and if I were creating a to do item, let's just say test item for team a. Here's a short description. I could assign this to a user which we'll come back and fix, but then I pick a team as well. Right?\u003C/p>\n\u003Cp>Now if I were a member of team a or team b, I wouldn't want to pick this, so we'll come back to that in a moment. But, basically, I can have a team that owns each one of these to dos. Great. So that's all set up. Right?\u003C/p>\n\u003Cp>How do we go in and manage those relationships? Because, you know, if we were to go in and create a user, let's just call it test user a for we'll we'll call it team a. Right? Team a user atexample.com. We're gonna do a real secure password here of password.\u003C/p>\n\u003Cp>And let's give them oh, the only role that we have now is administrator, so we'll sort that in a moment. But if we were to open this up and we go to team a user, so I'm in a incognito window. We'll go to password. Great. Okay.\u003C/p>\n\u003Cp>So and now you can see I could see all the teams, I can edit all the to dos. Right? So how do we solve for this problem? That is going to be using our user roles or the access control within Directus. So if we go to our access control, let's create a new role and call it user in this case.\u003C/p>\n\u003Cp>And they are going to have app access, you know, whether we enable app access or not. If I was building our front end and I I'm not sure if we're gonna have enough time to actually build the front end for this, but, you know, I could disable app access as I was using the APIs to to access this information. But we'll just call this user. And then we can go in and restrict our roles. Right?\u003C/p>\n\u003Cp>So now if I were to go in and, again, this is an incognito window on my left and the regular window on my right, so these have 2 different user sessions. But if I were to go in now to my team a user and change that from user or from administrator to user, you're gonna see that this person or this user, team a, does not see any content whatsoever and that is because we have not enabled any permissions for the user role. So we can go in and now let's start enabling some of these permissions. Right? So as far as Teams, we have create, read, update, delete, and the optional share, operation.\u003C/p>\n\u003Cp>But how do we how do we give them access to just the teams they're a part of? Right? Well, first, I need a relationship between our users and the teams, and and that's a piece that we forgot. Or did we? No.\u003C/p>\n\u003Cp>We've already fleshed that out. We've got our users there. Cool. So now I've got that relationship created, we can account for it inside our teams. Alright.\u003C/p>\n\u003Cp>So inside of permissions, let's go and we'll use if we use read access and we enable all access, what will we see? Right? Oh, that's the public permission. I guess I need to go into the user permission. Always love building against the clock.\u003C/p>\n\u003Cp>Right? You overlook some things. So here we can see all the teams now. That's not great because this user is a member of team a or at least should be. Let's make sure that we've got that set up.\u003C/p>\n\u003Cp>Right now they're a part of no team, so they can see all the teams, which is not helpful either. But let's make them a part of team a, and yet even though they're a part of team a, they can still see team a. So inside the that permissions, what we'll do, if if I actually go to the right one, we'll use the custom permission and we'll configure a rule that restricts that access. So the team, we can look and make sure that users, and I could drill into this, users dot directus user ID is one of, and we'll use a little shorthand here, current_user.id. So this is a dynamic variable within Directus that allows us to pick up the current user, so who is actually logged in, and we'll use the ID field.\u003C/p>\n\u003Cp>So this will allow us or should allow us to refresh and then we'll see that now this user can only see items or teams that are they're a part of, which is great. Right? They can't edit any of this information right now, but we could go in and restrict that as well. But just by doing this where we say our users and then we reference the user ID, which is gonna be an array in this case because we could have multiple users for one team, is one of dollar sign current underscore user dot ID. That allows us to build that permissions in.\u003C/p>\n\u003Cp>And I could even go through and restrict the specific fields that they could read about this as well if I didn't want them to have access to the slug or the other users, whatever. So now if we go into something like to dos, you know, we can enable all access for to dos, right, and we'll get the same effect where this one is assigned to a different team, they can still see that. So I think right now, this one is assigned to team a, I believe. And I could fix that as well where I go in, I go to my to dos and and instead of showing the actual ID, we wanna show the name of the team that's assigned to you. So we'll just on our display templates, we'll adjust that and go back, and we could see that, okay, this is assigned to team a.\u003C/p>\n\u003Cp>Let's create a new to do to do for team b. And again, I'm logged in as the administrator account over here. This is the individual user on the left. We'll save that. And if I were to go back to to dos now, they could still see to do for team b.\u003C/p>\n\u003Cp>Right? So how do we fix that? We go back into our access control and instead of having all access, again we use custom access. So we'll set up a rule where the team, and I could drill down and get the team dot ID, is again, we'll use is one of current user dot Teams? I can't remember what we named this, if it's team or team.\u003C/p>\n\u003Cp>Let's try team dot ID and see. Okay. So that breaks it to where they are not able to access any of it. Let's go back to our data model. We're gonna look at system collections, and we've got teams.\u003C/p>\n\u003Cp>Teams is the name of that. So if we go back, teams teams dot ID. Let's see if that sorts it out. It does not. Let's just check the syntax again.\u003C/p>\n\u003Cp>Is one of teams. Blah blah blah. Why is that not working? Team. Team team is okay.\u003C/p>\n\u003Cp>So we're hitting a snag here somewhere. Team dot ID. Let's try that. Team dotid is equal to current user dot teams is one of equals unexpected server error. So it has to be this one, Current user dot teams, or maybe it's just team.\u003C/p>\n\u003Cp>Alright. Let's take a look at our user directory. We've got our team. Teams hasn't been properly configured. Maybe something in the data model?\u003C/p>\n\u003Cp>Let's take a look. Teams is a many to many relationship. We've got team underscore users, team's ID. Oh, okay. Maybe that could be it.\u003C/p>\n\u003Cp>Team we've got to access through the junction collection, I believe. So we need to go back and, duh, we'll do current user dot team_users.directus. Nope. Team is teams_id. Team dotid.\u003C/p>\n\u003Cp>Let's try this. Team dot id is one of current user dot teams users or actually, maybe it is that teams dot ID. Still struggling. One of the the fine points of being against the clock. Right?\u003C/p>\n\u003Cp>We got 37 minutes, I'm going to cheat and not cheat, remember there are no rules, and we're just gonna reference one of the other, applications that I've built where I've referenced this before for a client role. So let's go in and look at this user dot contacts dot organizations dot ID, dot organizations dot underscore ID. So that is the collection there. That would be the organization's ID. Let's just take a look at the data model for this really quickly.\u003C/p>\n\u003Cp>Users. Okay. Trying to figure this out on a hurry. Users dot team users. No.\u003C/p>\n\u003Cp>Still really struggling here. Alright. So if we back this up, team, let's take a look at this one again one time. I just love building against the clock. Right?\u003C/p>\n\u003Cp>Let's let's just go back and evaluate what I've done. My brain is not thinking well today, I guess, so I haven't had enough coffee. So we'll go into our teams. We've got the users, so that's gonna be our junction collection. And then within the users collection or our junction table, it's gonna be we're identifying by the team's underscore ID.\u003C/p>\n\u003Cp>So thinking of it through that lens, our permission here instead of this whatever I've got here, it should just be something like this where it is the team underscore ID. So the team that's assigned to this to do, we get the ID from that team, and then we use the current user, and then we use the teams, which will be an array of objects, and then we get the teams underscore ID from that junction collection. So if I hit refresh, boom. Okay. Now we're cooking with gas.\u003C/p>\n\u003Cp>Let's just close this sidebar and continue building, right? So now I'm seeing only the to dos that have been assigned or or available to that specific team. Right? And if I want to see other users within that team, so within that junction collection, we're probably gonna need to do something similar here. So we can see users that are, where the teams dot id equals current user dot, oh, current underscore user dot teams dot teams underscore ID.\u003C/p>\n\u003Cp>So again, we're just referencing that relationship that we built and we'll probably do the same with our direct us users. Right? We don't want them to be able to see all the users. Where are you? Now I can't see any user.\u003C/p>\n\u003Cp>What's going on there? Test user for team a. Did we totally mess something up? Okay. So I could see these.\u003C/p>\n\u003Cp>We've got Let's go in and adjust the permissions for this. So we shouldn't be able to see, actually, that's gonna be on the the users collection anyway. Right? So users, we want the field permissions, the teams dot teams ID is one of current_user.teams._teams or dot teams underscore ID. Right.\u003C/p>\n\u003Cp>Great. So now we can only see users that are a part of the team that they're assigned to. Cool. Alright. So how do we actually go about creating to dos, right?\u003C/p>\n\u003Cp>This is not allowed inside this account. If we give all access, they should be able to create to dos now. But say we only we want to, restrict this team, right, or we wanna default this particular, team name, right, or team ID. What we could do is something like this where inside our to dos, we have field presets. So I could go in and say team is what, current, So we'll do this, like current underscore user dot teams dot ID.\u003C/p>\n\u003Cp>No. Dotteams_id. And maybe we get the first one, and that's how we assign. Let's see what this does just for testing. Alright.\u003C/p>\n\u003Cp>So we create a new to do. I should have access to this. Why is it saying we don't have permission access? Don't have permission to access this. Let's just try creating a test.\u003C/p>\n\u003Cp>Test to do. Hit save. Unexpected error occurred. Probably something to do with the preset that we've got saved here. Oh, I forgot the brackets.\u003C/p>\n\u003Cp>Could be it. Yeah. Still showing we do not have permission to access that. Great. So now we can go in and create this test to do.\u003C/p>\n\u003Cp>Alright. So we'll pick the team a, we'll assign that to team user a. Great. So now I can see those to dos. If we load up all the to dos in our master account or our administrator account, we could see that, I could see those to dos that are assigned for team b, but I cannot see those for team a because this user is not part of team a, right, and they cannot update that themselves.\u003C/p>\n\u003Cp>Great. So we would likewise just go through all of our different collections for our access control for the user role, and then we would just set those items. Right? So we could, whether we want this person to be able to edit Teams, yes or no. So again, I could copy this actual rule and I can get the raw value and I could use that.\u003C/p>\n\u003Cp>So we just copy paste this in. And now if I go to the team settings and I refresh, it doesn't look like this user can actually edit that. Users, direct as users is one of direct as users ID is 1 of okay. Oh, it's because there are no permissions enabled for the fields. Right?\u003C/p>\n\u003Cp>So now, with those field permissions updated, I can go in and this user can edit all of that information for that specific team. Likewise, for our to dos, we could go in and edit to dos. You know, you could set this up one of 2 ways where anybody within that team can edit this to do, or I could, you know, just let them edit to dos that we created or assigned. In this case, I'm just gonna use that same rule, copy and paste it in, and then I can go in and add all these fields that are available. Great.\u003C/p>\n\u003Cp>So we can edit those. You know, do we want to let them delete a team? Maybe we don't give them access to do that, but as far as the deleting to dos, go in and then as long as they're a member of that team, we can let them delete to dos in the account. So if I were to go here, now the user Team dot ID, that should be working. Teams dot ID.\u003C/p>\n\u003Cp>For some reason, this is not allowed for this specific user is one of current user dot Not sure why that is not available. It should be. Okay. If we give it all access, it is, But we're still logged in as that same user. So let's try to debug this a little bit, where team is one of current user dot teams, teams underscore ID.\u003C/p>\n\u003Cp>Now I can go in and delete that. Okay. So we've got that properly set up. Everything looks to be working correctly now. Where are we at, time wise?\u003C/p>\n\u003Cp>We got 26 minutes to, build out the rest of this SaaS application. Right? There's a a few other things, a few directions we could go, but as far as our functionality, we can create and manage tenants. We can create and manage users within those teams. And now we've made sure that all of our data is scoped to the individual team.\u003C/p>\n\u003Cp>And then, you know, technically they can go in and manage to do this. Right? But if I wanted to, flesh this out on the front end, like if I wanted to build an actual to do application on the front end, how would we make that work? Let let's just see how far we can get with that. So again, I've got a let me just close all the windows here.\u003C/p>\n\u003Cp>I've got this Nuxt application configured with a little bit of boilerplate already stood up just so we can take a look at how this works. But if I take a look, I've got a Directus module within this Nuxt application that is using the SDK. It does a few auto imports from the SDK and then just provides a composable called use Directus, that allows me to call the Directus SDK. You could of course call the API directly, but I like using the SDK just to standardize that across server or client runtimes. Alright.\u003C/p>\n\u003Cp>So if we take a look at our actual application, we've got nothing really fancy here. I do have an auth page. So if we go to auth/login, I've got a a simple login form that we can use. But, let's go open up our index page and start customizing this a bit, where if I wanted to read all of the tasks or the to dos from Directus. Alright.\u003C/p>\n\u003Cp>So I could do something like this where to dos equals await, what's my composable, use directus, and then we'll say read items. So I wanna read all the items from the to do's collection. And maybe I've just got, some options that will pass to that. And then here, let's just log out those to do's. Right?\u003C/p>\n\u003Cp>Let's see what this looks like. So if I open this up, you could see that we're getting an error, that says forbidden. So what does that actually mean? That is Directus telling us that the user for this application is not logged in. So what we could do is basically let's go to our login page, and let's actually just do this as well.\u003C/p>\n\u003Cp>So we'll get the user equals use state. This is a a helper with inside Nuxt, and I need to take a look at just what I've got set up here. It's just user. Alright. So we'll go back.\u003C/p>\n\u003Cp>We'll take a look at the user equals use state. And if I were to log the current user on this page, let's see what that shows. Maybe we just wait on that. We don't see anything at all. Right?\u003C/p>\n\u003Cp>So if I open up dev tools, we take a look at Vue, the user is undefined in this case. So let's just go in and let's log in using these credentials that we set up. Alright. So we look at our test user. We've got team a user at example with a very secure password, teammateuser@example.com with a password of password.\u003C/p>\n\u003Cp>I'm gonna log in. It's trying to redirect us to the portal, but that is not a page that we have right now. But now I can actually see the user because I am logged in. And if I were to just briefly swap these out, where we're just logging to do's, now I can actually see those to do's as well. So now that we're logged in, we're getting that information from Directus.\u003C/p>\n\u003Cp>Right? So let's do 2 things here and just check the clock very quickly. We're at 22 minutes. How do we set this up? Alright.\u003C/p>\n\u003Cp>So I'm just gonna sketch these out really quickly. We're going to show a list of to dos, and then we're going to have the form show a form to add to dos. This is your standard to do list functionality. Alright. So we got the user.\u003C/p>\n\u003Cp>If use if there's no user, let's just redirect to the login page. That's a good idea. Thank you, GitHub Copilot. And that will be await navigate to /auth/login. And, of course, if I was actually building this for real, I would take great care to modularize this and and extract some of this logic so it could be reused.\u003C/p>\n\u003Cp>You know, I'd probably stick this in a Nuxt middleware or something like that. So now if I were to go in and just delete the cookie that we're storing for the user, which is in here where it says direct us off, If I just nuke all the cookies, what's going on? Forbidden. If no user, maybe we need to wrap this up. Else, we're gonna fetch the oh, there is no argument for else.\u003C/p>\n\u003Cp>So this is still showing forbidden. Maybe we return dot navigate 2. If there is no user, just navigate dot 2. I don't understand what's going on here. If we nuke all this, should not be getting forbidden.\u003C/p>\n\u003Cp>Right? If user user dot ID, maybe. Okay. There we go. User was actually defined.\u003C/p>\n\u003Cp>There just weren't any properties within it. So we don't have a user ID. We're gonna redirect. Great. So again, if we do team a user at example.com, password, and log in.\u003C/p>\n\u003Cp>That's gonna redirect us to the portal. But if we go back to the home page, We logged in, do we not? Okay. Let's just scrap this for now. We'll go back to the home page, see what's going on.\u003C/p>\n\u003Cp>Okay. Great. So we want to show a list of to do's in this case, and we will do ul, to do list for to dos and to dos. Let's pick up the name. Then we have, let's actually wrap this in a p tag.\u003C/p>\n\u003Cp>We'll make this, what, text excel. Make it large. We'll make it bold. Great. Then we'll do a div and we have the description of that.\u003C/p>\n\u003Cp>So vhtml.todo.description. Prefer the self closing tags personally. And we'll use like the the Tailwind Pros class because I've got that set up. Here's the short description of this. Maybe we add a input type checkbox for to do.\u003C/p>\n\u003Cp>Just call it status. Alright. And let's go ahead and flex those 2. Right? So we'll wrap these again.\u003C/p>\n\u003Cp>Okay. Give it a little bit of gap and some padding and maybe a border. Let's start the items at the top. Cool. And maybe we wrap that as well.\u003C/p>\n\u003Cp>Okay. So now we got a list of to dos. How can we actually push this into our to do list? Let's add a form for rendering those to dos or how to add those to dos inside our list. So we got a form, thank you, to do dot name, to do dot description, submit button, add to do.\u003C/p>\n\u003Cp>Thank you, GitHub Copilot. Let's take a look at what we've got now. Cannot add properties of to name. That's because we don't have a to do. Right?\u003C/p>\n\u003Cp>So we'll go in. Let's add a new to do. This will just be a reactive object. And we need to change our v model from to do dot name. We'll do new to do dot name.\u003C/p>\n\u003Cp>That way we can keep everything savvy. That looks kind of rough. Not very pretty. Right? Let's change this.\u003C/p>\n\u003Cp>One of the UI libraries that I have baked into this starter is this is Nuxt UI library. Very handy little piece of kit. We got 16 minutes left. How can we show something impressive if we look at this library? Where did you go?\u003C/p>\n\u003Cp>There's like a a form group component or a form input already here. So we got a form group that gives us a label, and then we just wrap that. Okay. So let's take a look at that. We'll just open this up.\u003C/p>\n\u003Cp>We'll do new form group. We've got the name. That'll be an input. Title of the task or to do. We don't really need an icon for this, and we'll just v model out new to do dot name.\u003C/p>\n\u003Cp>And we'll do the same for description. Alright. So we'll just model this up. Description. Description of the task to complete.\u003C/p>\n\u003Cp>And we'll just do description. And then we've got, we'll just use their button component as well. Cool. Let's clean this up a little bit. Actually, let's add some padding to the whole form.\u003C/p>\n\u003Cp>So let's do something like p 8. Maybe we add a header here. I've got a built in VText component here. Let's make the size large, say task. And then we add some spacing between these items, space y 8.\u003C/p>\n\u003Cp>Alright. So we've got the form. We've got the actual task here. That's great. Alright.\u003C/p>\n\u003Cp>So this is a submit type, on our form. Maybe we wanna do at submit dot prevent, and then we're gonna build a method to submit that to do to our Directus API. So submit. Let's just call it add new to do. And let's go in and build this.\u003C/p>\n\u003Cp>Right? So we'll call this an async function, add new to do. And what is GitHub Copilot suggesting? We will, await, create item, add to dos, new to do. So we're gonna pass that.\u003C/p>\n\u003Cp>And then it is clearing out the items within that new to do so we can add a new one. This looks pretty good. Let's add one final thing. Maybe we want to wrap this in a try catch. We'll error out.\u003C/p>\n\u003Cp>And then the last thing that we probably wanna do is actually update the to do list. Right? So, one thing we can do here is wrap this function. Let's say async function, if I can actually spell, fetch to dos. Return data, get items to dos.\u003C/p>\n\u003Cp>Actually, let's just swap that. We'll return the data from that. Great. We'll log any errors. And now we're not seeing any to dos because we actually need to call that during the setup of this.\u003C/p>\n\u003Cp>So we'll call const to dos equals ref. And I could actually do something like this if we wanted to make it reactive where I just have to do's at the top here. And within this function, I could, to do's dot value, just update those to do's, which let's do that. And here, we'll say, wait. And this works because we are in we're using script setup with inside Nuxt review.\u003C/p>\n\u003Cp>So we could do fetch to do's. That will fetch the to do's when it sets up this component. And then when we add a new to do, we're going to fetch those to do's again. So if I go in and now let's test this out. New to do.\u003C/p>\n\u003Cp>Do a description here. We hit add to do. K. It did something. What did it actually do?\u003C/p>\n\u003Cp>Input types contains is not a function. No idea what we're doing here. Form at submit dot prevent is add new to do. This is a submit button. Got new to do dot name dot description.\u003C/p>\n\u003Cp>Why is this not showing? Right? Let's try try it again. Test to do. We'll look at our fetch request description, and it looks like this is okay.\u003C/p>\n\u003Cp>Post items to do's, payload status equals false. Right? What's what's the issue here? Why are we running into an error? And you could see because I've logged in as a different user in my application, Directus has also logged me out.\u003C/p>\n\u003Cp>So we'll just log back in really quickly. And again, we could see that the to dos are being created, but they are not being assigned to a team. Right? So we got 10 minutes and 30 seconds left to resolve this issue. If we look at our user, we could see that we have the team's array.\u003C/p>\n\u003Cp>Right? So we have the ID of that team. We just need to pass that along inside the request. You know, if if I did not have, like, a setup where a one user could be involved in multiple teams, you know, maybe I have a a preset within the API itself to to do that. You know, if if I've got a user that is a part of multiple teams, we have to pass that along, because it's not gonna know which team.\u003C/p>\n\u003Cp>So here inside this, we could also do something like this where we have a switch for this or, let's just pick up the team from the user. Right? So we've got the user here inside new to do, new to do dot team. Let's add the user dot, teams. It's just the first item inside the array.\u003C/p>\n\u003Cp>So something like this. Alright. Let's see. Test. Test.\u003C/p>\n\u003Cp>Test. Test. Test. Does that actually work? Looks like we're not getting any response from API team.\u003C/p>\n\u003Cp>Okay. There's the team. Okay. It's because we are the team is not an array. It's just a single value.\u003C/p>\n\u003Cp>Okay. Try it again. Test. Test. Add to do.\u003C/p>\n\u003Cp>What are we not doing? Right? So the team is actually we're not calling that data at all. New to do dot team equals user dot teams. Is that because that is a value?\u003C/p>\n\u003Cp>Let's refresh the page. Just test. To do's. Okay, it's it's not performing our post request. Cannot read values of undefined.\u003C/p>\n\u003Cp>So it's probably something like this. Let's just unref that user Dot teams 0. Test this again. To do's is a bad request. Okay.\u003C/p>\n\u003Cp>So we're passing something that Teams does not like. The team status is false. Why why is it not liking that at all? Team so if you look at our access control, let's try to diagnose this. We got the to dos.\u003C/p>\n\u003Cp>We can create to do's. We got access to create all the fields. And if we just delete some of these other ones, What's going on? Why can't we assign a to do a new to do to team a? Right?\u003C/p>\n\u003Cp>We should be able to do that. Test test test test test test. Todos is status is is false. Maybe that's it. Test.\u003C/p>\n\u003Cp>Test. It still does not like the fact. Invalid foreign key for the team. Okay. So maybe it should be an array then.\u003C/p>\n\u003Cp>Let's test again. Do not have permission to access this. Invalid foreign key for the team to dos. Why are we seeing this particular issue? Right?\u003C/p>\n\u003Cp>What are where are we at on the clock? We got 6 minutes. Let's resolve this thing and finish this one up. Right? So let's just leave the status out for now.\u003C/p>\n\u003Cp>Why are we not getting the proper response from Directus? Alright. So we go in, we look at our data model for our to dos. We've got a team. That's a mini to 1.\u003C/p>\n\u003Cp>If we just were to, like, log this out, let's say we give access to public access for all of our to dos, all the teams. And just to debug this a little bit, if I go into items, I do to dos, I could see that team is just, a single string. That's the UID for that string. And it should be one of these values, like 8bf or ending in a 69e. What are we doing?\u003C/p>\n\u003Cp>What are we passing inside that request? Test. Okay. Now I'm logged in as the the user again. Let me clear all the cookies.\u003C/p>\n\u003Cp>That could be what's going on. You know, if you're working with Directus in one window, you're using the API in another window, maybe you want to make sure that you're using incognito windows. So when you log in that you're not seeing something outrageous here. Let's clear our cookies out. Oh, and now we can see all the to do's because we are not logged in.\u003C/p>\n\u003Cp>And I could set up those permissions. So let's restrict those permissions again. We're just gonna remove all those. Great. And now we log in.\u003C/p>\n\u003Cp>We do team a user at example.com, go to password, go back a step, now we've got our to dos. Let's test. Alright. We're still getting the same thing. You don't have permission to access this.\u003C/p>\n\u003Cp>That's because it's not an array. We refresh, test, test. Again, invalid foreign key. Right? So what are we actually getting here for the team?\u003C/p>\n\u003Cp>If I look at our user, teams dot okay. So we have the user there. I don't understand why we're not able to access the value. Oh, let's add a team. We'll just give it a false string.\u003C/p>\n\u003Cp>User let's try user dot value dot teams dot 0. Maybe we just actually console log the the user. The user is right. Let's take a look at that. Alright.\u003C/p>\n\u003Cp>So we've got our to dos. We had test to do. Alright. So there's our user. Teams.\u003C/p>\n\u003Cp>Alright. Bad requests. Invalid foreign key for team in collection to dos. Something to do with passing the wrong team. So if we take a look at our teams, team a, Why are we getting the wrong value here?\u003C/p>\n\u003Cp>So team a should be this particular value. We're sending 5 e. Is that correct? 4e? No.\u003C/p>\n\u003Cp>I I don't know why we're where this value is coming from for teams for that specific user. Right? Team a user, do we add that user to a team that is not available? Team a. Oh, duh.\u003C/p>\n\u003Cp>That's because we're not actually getting the team user, we are getting the junction collection. So great idea, Brian. Let's go into our config for this and it actually should be the junction collection. So the, we need to access the team from the junction collection instead of the just the okay. Alright.\u003C/p>\n\u003Cp>So we can fix this. We'll go into our Nuxt config. Where are our fields for this? User fields, we're going to pick up the teams dot okay. So let's just refresh.\u003C/p>\n\u003Cp>Come on. Come on. Let's build. We got 30 seconds left to fix it. Come on, build, baby.\u003C/p>\n\u003Cp>Okay. So now within our function, we are going to go in and call instead of teams okay, then we're gonna use teams underscore ID. That should be it. And all these issues probably would have been prevented had we used TypeScript. But now we could see that.\u003C/p>\n\u003Cp>And with 54321 on the clock, you could see as I add to dos, boom, it is fetching those and updating them. Wow. So right at the clock, we have finished the multi tenant to do application. What would our next steps for this sort of thing be? Right?\u003C/p>\n\u003Cp>One probably hooking up subscriptions and and actually fleshing out this logic. So as far as next steps, let's just discuss those for a few. You know, add subscriptions. I can unstriker through those. We would add our subscriptions using something like Stripe.\u003C/p>\n\u003Cp>Clean up all of our data fetching, fetching and handling, and probably extract some of that logic into a middleware. But the major achievement here is being able to create that multi tenant role and permission setup within an hour, so that each user can only see their data. They can only interact with that data. So, great. You know, we took all 60 minutes on this one.\u003C/p>\n\u003Cp>The UI doesn't look fantastic, but as far as our back end, pretty much ready to go. I hope you'll join in for another episode of 100 apps 100 hours. Until then, I'll see you around.\u003C/p>","Welcome back to the next episode of 100 apps, 100 hours where we rebuild or recreate some of your favorite apps in 60 minutes or less or publicly fail trying. Sounds really scary when we say it that way. I am your host, Brian Gillespie, developer advocate at Directus, and today, we're gonna be building a multi tenant SaaS application. I realize that is not everybody's most favorite app, but it is the architecture that powers a lot of your favorite apps, and it's a question that comes up a lot within our community. So by multi tenant, what do we actually mean when we say that? It means that we have multiple different accounts or different tenants in this case, so we serve multiple different groups of users in one single application. And all of their data is separated. It's, you know, team a doesn't see the data or can't interact with the data for team b. It is totally doable on our back end as we're gonna see, and it's a popular convention for a lot of the SaaS apps that you will use so that we could share resources like a database, you know, an actual front end application without having to, you know, commission a brand new database for every single account that we have. Alright. Sounds great, sounds interesting, something that we want to build. How do we build it? What are the rules here? If you're new to the series, there are only 2 rules. We have 60 minutes to plan and build an application, no more, no less. And I do like to include a little planning ahead of time so we know, or before we actually build, not ahead of time. We're starting fresh here. You've got as much of a clean slate as I do. And then the second rule is use whatever you have at your disposal, whether that's Tailwind, online resources, AI, anything is fair game. We've got to get as far as we can in 60 minutes or less or look like an idiot. So sounds great. Everybody knows the rules. We're gonna be building the multi tenant SaaS app. Let's open this up and fire off the countdown. Here we go. So before we actually touch any code or any data models or, you know, anything in our back end, let's go through and and plan this out a little bit. And usually I like to start with our functionality. So let's shrink this down a little bit. And what functionality are we looking for out of this application? We want to create and manage tenants. In this case, let's call it, Teams is a pretty good convention. You know, accounts would be one that I see often organizations and in some applications. We wanna create and manage users within those teams. We wanna make sure that all data, make sure all data is scoped to each team. Right? So team a can access team b's data and vice versa to the team. And then, you know, a SaaS app needs to do something, so let's do just like the standard to dos. So we wanna create and manage to dos. Perfect. Alright. So there's our functionality. Nothing particularly fancy but it is tricky to set up some of this, especially around permissions. You know, that is a a big piece of the puzzle. Security is always very important. So, you know, how how do we set this up and how do we do it within an hour? Let's go through and now let's let's work on our data model a little bit. So we got our data model. And what are we gonna need as far as our tables or our different collections that we're gonna be using? So we will have, what, teams. Let's shrink this up a little bit. We will have users, we'll have to do's, and you know, we may not get to this but if I was gonna actually have this be built as a SaaS that I could charge people for, we'd probably have something like subscriptions. And we didn't include that in the functionality because I'm not sure exactly how far we can get in an hour, but we'll get to as much as possible. Right? So in this scenario, a user could be part of one team or, technically I guess we could build it so that a user can be in multiple teams. Multiple teams. Let's break that in half so we can actually see that. To do's belong to a team. To do's belong to a team assigned to a user. And then there's probably a a junction table here that is what? Users and teams. Oh, let's use something that's not connected. Right? So this is probably a table called something like teams users because it is a many to many relationship and just to get fancy and finish out our little arrows could look something like this. I'm definitely not an artist but you get the picture. Alright. So we've got teams, we've got users, we have to do's, we have team underscore users or or some of junction collection. We potentially have, subscriptions. So what do we have set up to actually build this? I'm using Directus for the back end of the application. I've got a super simple Nuxt application for the front end that we may or may not get to that and I've just got a little boiler plate that, uses the Directus SDK for that information. Let's jump right in. So if I pull up my Docker Compose and that's how I've got Directus set up locally, just through a simple Docker Compose file that you pull down from the documentation. And I'm gonna get logged in to our first user, and I'll just copy and paste this password here because I suck at typing a lot of times. Alright. So you can see that we've got a blank application. There is no data model. We've got a single admin user. We're starting from a clean slate. So if I just pull this up and we rearrange all these windows, maybe let's pull this over to the side so we can actually see what we're building, And I'll zoom in a little bit so you've got a better view. Maybe we don't put the clock in our face, so we're so we're not beating up against the clock. But how do we start? You know, let's start by creating this tenant collection. I'm gonna call this teams. You could easily call it tenants. You probably want to whatever makes the most sense in the context of your application is probably what I would use. Right? So the teams here, we're gonna generate a primary key called ID. We're just gonna use a UUID for that. As far as the status of that, yeah, we probably want a status for the team. We don't need a sort field. And then these others are just system fields that you can create, to give you a little shortcut. You know, when was it created? What was the user that created it? This specific team, blah blah blah. Alright. So as far as the team, what are we gonna need for the team information? We'll probably need a name for that team. That's great. Maybe we want to have a slug for the team. Could be a a good one to have. That way, we're not using a weird UUID, but instead a slug for the team. And let's go in and, adjust this a little bit. We're going to look for the interface section and Directus has the slugify option to make sure that slug input is URL safe. And one of the other things I'm gonna do here is just open up my little mouse pose tool. If you guys are ever wondering how I do this little highlight on the screen, that is a tool called mouse pose, very handy piece of kit. Alright so we've got our teams, now let's add our users, right? I could create a separate user collection but when I fire up Directus, it gives me a system collection for users that I can use to actually I could just reuse that for all of the authentication, all of the roles and permissions because what happens behind the scenes with Directus as my back end is it will mirror all these tables or these collections that we're creating inside the app to MySQL database, which is super handy. I I keep MySQL pure and, MySQL databases remains pure. And I've got built in REST and GraphQL APIs that are also automatically generated based on this data model. So tremendously powerful, enables you to build really quickly. So we're just gonna use that that system collection. I'm gonna I am gonna go in and create a new one. Let's just call it team users, and we'll generate a UID for that. We'll leave that one blank for now. Let's go in and add our to do's. So each to do we'll need, who's the date created, who was the user created it. We wanna give those a status as well, maybe a sort, so we can rank those in order of priority. So we've got the bare bones of our data model here, and we've got the again, we're gonna use the directus users system collection. How do we go and actually start setting up the tenancy? Right? Because right now, if anyone were to log in to this application, they would see all the data that we have available. So first, we are going to go in and create our relationships between these different collections, or which are just equivalent to tables within your Postgres database, let's first go through and do the teams and users. So a user can be in multiple teams. Let's go in and model that relationship. So we'll hit create field. We're gonna look for the many to many relationship inside Directus here, and let's just call this our users. Alright so the related collection is going to be users as well, actually it's not. It's gonna be directus_users, and that'll turn purple letting me know that that is an actual collection. And then we probably wanna show a link to the item. But instead of hitting save here, I'm going to hit continue in advanced field mode or advanced mode because I want to control what my Junction collection is named. So I'll just go in, we've already got this, it's called team underscore users, And I could go in and edit the, the foreign key fields here as well. It's just gonna be teams underscore ID, directus underscore users underscore ID. That's fine. Not a big deal. And if I wanted to, I can also add the reverse or the inverse relationship. Somebody will correct me on the comments. But, I can add that back to the Directus users collection so that I can reference that in any of our permissions that we're going to use later. We probably don't necessarily need a sort field, not a big deal. And the rest of this is pretty standard. So let's go ahead and save that. And now we have our users that are linked to, that direct us users table. So if I were to go in and create a team, I can see that I can add existing users or create new users for that team. Great. Alright. So next, let's take a look at teams and to dos. Right? So the to dos belong to a single team and I can assign those to dos to a user. So if we flesh out this data model a little bit more, we probably got a title or a name for this to do, let's use that. We probably have a description, so for that we'll just use our WYSIWYG editor inside the back end here. And I can even control what options we have as far as HTML content, but we'll just keep this the standard one for now. Alright. And then we've probably got a user that this has been assigned to. So we're gonna scroll down to our relational section. And depending on how we wanted to assign these, if we wanted to assign this task to or this to do to many users, you know, I would use either like a many to 1 or a one to many. In this case I just want to assign it to a single user. You know, whoever we assign this to is ultimately responsible for getting this thing done, and we're just gonna call this assign to. So we're going to use the many to one relationship within Directus, call it the assign to, and our related collection is going to be Directus users. So we'll hit save, that's who we assign this to. And now, let's go in and make sure this is applied to a team. So we need that relationship to a team so we can actually filter that out. Right? If team a is logged in or a user on team a is logged in, we don't want them to be able to see or interact with any of the to dos for team b, unless they are part of that team, which makes it a little more complicated to model as well. But we'll just call this, team or, you know, it could be team ID. Whatever. I'm gonna I'm gonna stick with team, and then the related collection is teams. Straightforward enough. Right? So if I were to go in now and we create a to do actually, let's just first create our team. Right? I've been talking about team a, there's the slug for team a, and team b. Cool. So now we have 2 teams, And I can go in and if I were creating a to do item, let's just say test item for team a. Here's a short description. I could assign this to a user which we'll come back and fix, but then I pick a team as well. Right? Now if I were a member of team a or team b, I wouldn't want to pick this, so we'll come back to that in a moment. But, basically, I can have a team that owns each one of these to dos. Great. So that's all set up. Right? How do we go in and manage those relationships? Because, you know, if we were to go in and create a user, let's just call it test user a for we'll we'll call it team a. Right? Team a user atexample.com. We're gonna do a real secure password here of password. And let's give them oh, the only role that we have now is administrator, so we'll sort that in a moment. But if we were to open this up and we go to team a user, so I'm in a incognito window. We'll go to password. Great. Okay. So and now you can see I could see all the teams, I can edit all the to dos. Right? So how do we solve for this problem? That is going to be using our user roles or the access control within Directus. So if we go to our access control, let's create a new role and call it user in this case. And they are going to have app access, you know, whether we enable app access or not. If I was building our front end and I I'm not sure if we're gonna have enough time to actually build the front end for this, but, you know, I could disable app access as I was using the APIs to to access this information. But we'll just call this user. And then we can go in and restrict our roles. Right? So now if I were to go in and, again, this is an incognito window on my left and the regular window on my right, so these have 2 different user sessions. But if I were to go in now to my team a user and change that from user or from administrator to user, you're gonna see that this person or this user, team a, does not see any content whatsoever and that is because we have not enabled any permissions for the user role. So we can go in and now let's start enabling some of these permissions. Right? So as far as Teams, we have create, read, update, delete, and the optional share, operation. But how do we how do we give them access to just the teams they're a part of? Right? Well, first, I need a relationship between our users and the teams, and and that's a piece that we forgot. Or did we? No. We've already fleshed that out. We've got our users there. Cool. So now I've got that relationship created, we can account for it inside our teams. Alright. So inside of permissions, let's go and we'll use if we use read access and we enable all access, what will we see? Right? Oh, that's the public permission. I guess I need to go into the user permission. Always love building against the clock. Right? You overlook some things. So here we can see all the teams now. That's not great because this user is a member of team a or at least should be. Let's make sure that we've got that set up. Right now they're a part of no team, so they can see all the teams, which is not helpful either. But let's make them a part of team a, and yet even though they're a part of team a, they can still see team a. So inside the that permissions, what we'll do, if if I actually go to the right one, we'll use the custom permission and we'll configure a rule that restricts that access. So the team, we can look and make sure that users, and I could drill into this, users dot directus user ID is one of, and we'll use a little shorthand here, current_user.id. So this is a dynamic variable within Directus that allows us to pick up the current user, so who is actually logged in, and we'll use the ID field. So this will allow us or should allow us to refresh and then we'll see that now this user can only see items or teams that are they're a part of, which is great. Right? They can't edit any of this information right now, but we could go in and restrict that as well. But just by doing this where we say our users and then we reference the user ID, which is gonna be an array in this case because we could have multiple users for one team, is one of dollar sign current underscore user dot ID. That allows us to build that permissions in. And I could even go through and restrict the specific fields that they could read about this as well if I didn't want them to have access to the slug or the other users, whatever. So now if we go into something like to dos, you know, we can enable all access for to dos, right, and we'll get the same effect where this one is assigned to a different team, they can still see that. So I think right now, this one is assigned to team a, I believe. And I could fix that as well where I go in, I go to my to dos and and instead of showing the actual ID, we wanna show the name of the team that's assigned to you. So we'll just on our display templates, we'll adjust that and go back, and we could see that, okay, this is assigned to team a. Let's create a new to do to do for team b. And again, I'm logged in as the administrator account over here. This is the individual user on the left. We'll save that. And if I were to go back to to dos now, they could still see to do for team b. Right? So how do we fix that? We go back into our access control and instead of having all access, again we use custom access. So we'll set up a rule where the team, and I could drill down and get the team dot ID, is again, we'll use is one of current user dot Teams? I can't remember what we named this, if it's team or team. Let's try team dot ID and see. Okay. So that breaks it to where they are not able to access any of it. Let's go back to our data model. We're gonna look at system collections, and we've got teams. Teams is the name of that. So if we go back, teams teams dot ID. Let's see if that sorts it out. It does not. Let's just check the syntax again. Is one of teams. Blah blah blah. Why is that not working? Team. Team team is okay. So we're hitting a snag here somewhere. Team dot ID. Let's try that. Team dotid is equal to current user dot teams is one of equals unexpected server error. So it has to be this one, Current user dot teams, or maybe it's just team. Alright. Let's take a look at our user directory. We've got our team. Teams hasn't been properly configured. Maybe something in the data model? Let's take a look. Teams is a many to many relationship. We've got team underscore users, team's ID. Oh, okay. Maybe that could be it. Team we've got to access through the junction collection, I believe. So we need to go back and, duh, we'll do current user dot team_users.directus. Nope. Team is teams_id. Team dotid. Let's try this. Team dot id is one of current user dot teams users or actually, maybe it is that teams dot ID. Still struggling. One of the the fine points of being against the clock. Right? We got 37 minutes, I'm going to cheat and not cheat, remember there are no rules, and we're just gonna reference one of the other, applications that I've built where I've referenced this before for a client role. So let's go in and look at this user dot contacts dot organizations dot ID, dot organizations dot underscore ID. So that is the collection there. That would be the organization's ID. Let's just take a look at the data model for this really quickly. Users. Okay. Trying to figure this out on a hurry. Users dot team users. No. Still really struggling here. Alright. So if we back this up, team, let's take a look at this one again one time. I just love building against the clock. Right? Let's let's just go back and evaluate what I've done. My brain is not thinking well today, I guess, so I haven't had enough coffee. So we'll go into our teams. We've got the users, so that's gonna be our junction collection. And then within the users collection or our junction table, it's gonna be we're identifying by the team's underscore ID. So thinking of it through that lens, our permission here instead of this whatever I've got here, it should just be something like this where it is the team underscore ID. So the team that's assigned to this to do, we get the ID from that team, and then we use the current user, and then we use the teams, which will be an array of objects, and then we get the teams underscore ID from that junction collection. So if I hit refresh, boom. Okay. Now we're cooking with gas. Let's just close this sidebar and continue building, right? So now I'm seeing only the to dos that have been assigned or or available to that specific team. Right? And if I want to see other users within that team, so within that junction collection, we're probably gonna need to do something similar here. So we can see users that are, where the teams dot id equals current user dot, oh, current underscore user dot teams dot teams underscore ID. So again, we're just referencing that relationship that we built and we'll probably do the same with our direct us users. Right? We don't want them to be able to see all the users. Where are you? Now I can't see any user. What's going on there? Test user for team a. Did we totally mess something up? Okay. So I could see these. We've got Let's go in and adjust the permissions for this. So we shouldn't be able to see, actually, that's gonna be on the the users collection anyway. Right? So users, we want the field permissions, the teams dot teams ID is one of current_user.teams._teams or dot teams underscore ID. Right. Great. So now we can only see users that are a part of the team that they're assigned to. Cool. Alright. So how do we actually go about creating to dos, right? This is not allowed inside this account. If we give all access, they should be able to create to dos now. But say we only we want to, restrict this team, right, or we wanna default this particular, team name, right, or team ID. What we could do is something like this where inside our to dos, we have field presets. So I could go in and say team is what, current, So we'll do this, like current underscore user dot teams dot ID. No. Dotteams_id. And maybe we get the first one, and that's how we assign. Let's see what this does just for testing. Alright. So we create a new to do. I should have access to this. Why is it saying we don't have permission access? Don't have permission to access this. Let's just try creating a test. Test to do. Hit save. Unexpected error occurred. Probably something to do with the preset that we've got saved here. Oh, I forgot the brackets. Could be it. Yeah. Still showing we do not have permission to access that. Great. So now we can go in and create this test to do. Alright. So we'll pick the team a, we'll assign that to team user a. Great. So now I can see those to dos. If we load up all the to dos in our master account or our administrator account, we could see that, I could see those to dos that are assigned for team b, but I cannot see those for team a because this user is not part of team a, right, and they cannot update that themselves. Great. So we would likewise just go through all of our different collections for our access control for the user role, and then we would just set those items. Right? So we could, whether we want this person to be able to edit Teams, yes or no. So again, I could copy this actual rule and I can get the raw value and I could use that. So we just copy paste this in. And now if I go to the team settings and I refresh, it doesn't look like this user can actually edit that. Users, direct as users is one of direct as users ID is 1 of okay. Oh, it's because there are no permissions enabled for the fields. Right? So now, with those field permissions updated, I can go in and this user can edit all of that information for that specific team. Likewise, for our to dos, we could go in and edit to dos. You know, you could set this up one of 2 ways where anybody within that team can edit this to do, or I could, you know, just let them edit to dos that we created or assigned. In this case, I'm just gonna use that same rule, copy and paste it in, and then I can go in and add all these fields that are available. Great. So we can edit those. You know, do we want to let them delete a team? Maybe we don't give them access to do that, but as far as the deleting to dos, go in and then as long as they're a member of that team, we can let them delete to dos in the account. So if I were to go here, now the user Team dot ID, that should be working. Teams dot ID. For some reason, this is not allowed for this specific user is one of current user dot Not sure why that is not available. It should be. Okay. If we give it all access, it is, But we're still logged in as that same user. So let's try to debug this a little bit, where team is one of current user dot teams, teams underscore ID. Now I can go in and delete that. Okay. So we've got that properly set up. Everything looks to be working correctly now. Where are we at, time wise? We got 26 minutes to, build out the rest of this SaaS application. Right? There's a a few other things, a few directions we could go, but as far as our functionality, we can create and manage tenants. We can create and manage users within those teams. And now we've made sure that all of our data is scoped to the individual team. And then, you know, technically they can go in and manage to do this. Right? But if I wanted to, flesh this out on the front end, like if I wanted to build an actual to do application on the front end, how would we make that work? Let let's just see how far we can get with that. So again, I've got a let me just close all the windows here. I've got this Nuxt application configured with a little bit of boilerplate already stood up just so we can take a look at how this works. But if I take a look, I've got a Directus module within this Nuxt application that is using the SDK. It does a few auto imports from the SDK and then just provides a composable called use Directus, that allows me to call the Directus SDK. You could of course call the API directly, but I like using the SDK just to standardize that across server or client runtimes. Alright. So if we take a look at our actual application, we've got nothing really fancy here. I do have an auth page. So if we go to auth/login, I've got a a simple login form that we can use. But, let's go open up our index page and start customizing this a bit, where if I wanted to read all of the tasks or the to dos from Directus. Alright. So I could do something like this where to dos equals await, what's my composable, use directus, and then we'll say read items. So I wanna read all the items from the to do's collection. And maybe I've just got, some options that will pass to that. And then here, let's just log out those to do's. Right? Let's see what this looks like. So if I open this up, you could see that we're getting an error, that says forbidden. So what does that actually mean? That is Directus telling us that the user for this application is not logged in. So what we could do is basically let's go to our login page, and let's actually just do this as well. So we'll get the user equals use state. This is a a helper with inside Nuxt, and I need to take a look at just what I've got set up here. It's just user. Alright. So we'll go back. We'll take a look at the user equals use state. And if I were to log the current user on this page, let's see what that shows. Maybe we just wait on that. We don't see anything at all. Right? So if I open up dev tools, we take a look at Vue, the user is undefined in this case. So let's just go in and let's log in using these credentials that we set up. Alright. So we look at our test user. We've got team a user at example with a very secure password, teammateuser@example.com with a password of password. I'm gonna log in. It's trying to redirect us to the portal, but that is not a page that we have right now. But now I can actually see the user because I am logged in. And if I were to just briefly swap these out, where we're just logging to do's, now I can actually see those to do's as well. So now that we're logged in, we're getting that information from Directus. Right? So let's do 2 things here and just check the clock very quickly. We're at 22 minutes. How do we set this up? Alright. So I'm just gonna sketch these out really quickly. We're going to show a list of to dos, and then we're going to have the form show a form to add to dos. This is your standard to do list functionality. Alright. So we got the user. If use if there's no user, let's just redirect to the login page. That's a good idea. Thank you, GitHub Copilot. And that will be await navigate to /auth/login. And, of course, if I was actually building this for real, I would take great care to modularize this and and extract some of this logic so it could be reused. You know, I'd probably stick this in a Nuxt middleware or something like that. So now if I were to go in and just delete the cookie that we're storing for the user, which is in here where it says direct us off, If I just nuke all the cookies, what's going on? Forbidden. If no user, maybe we need to wrap this up. Else, we're gonna fetch the oh, there is no argument for else. So this is still showing forbidden. Maybe we return dot navigate 2. If there is no user, just navigate dot 2. I don't understand what's going on here. If we nuke all this, should not be getting forbidden. Right? If user user dot ID, maybe. Okay. There we go. User was actually defined. There just weren't any properties within it. So we don't have a user ID. We're gonna redirect. Great. So again, if we do team a user at example.com, password, and log in. That's gonna redirect us to the portal. But if we go back to the home page, We logged in, do we not? Okay. Let's just scrap this for now. We'll go back to the home page, see what's going on. Okay. Great. So we want to show a list of to do's in this case, and we will do ul, to do list for to dos and to dos. Let's pick up the name. Then we have, let's actually wrap this in a p tag. We'll make this, what, text excel. Make it large. We'll make it bold. Great. Then we'll do a div and we have the description of that. So vhtml.todo.description. Prefer the self closing tags personally. And we'll use like the the Tailwind Pros class because I've got that set up. Here's the short description of this. Maybe we add a input type checkbox for to do. Just call it status. Alright. And let's go ahead and flex those 2. Right? So we'll wrap these again. Okay. Give it a little bit of gap and some padding and maybe a border. Let's start the items at the top. Cool. And maybe we wrap that as well. Okay. So now we got a list of to dos. How can we actually push this into our to do list? Let's add a form for rendering those to dos or how to add those to dos inside our list. So we got a form, thank you, to do dot name, to do dot description, submit button, add to do. Thank you, GitHub Copilot. Let's take a look at what we've got now. Cannot add properties of to name. That's because we don't have a to do. Right? So we'll go in. Let's add a new to do. This will just be a reactive object. And we need to change our v model from to do dot name. We'll do new to do dot name. That way we can keep everything savvy. That looks kind of rough. Not very pretty. Right? Let's change this. One of the UI libraries that I have baked into this starter is this is Nuxt UI library. Very handy little piece of kit. We got 16 minutes left. How can we show something impressive if we look at this library? Where did you go? There's like a a form group component or a form input already here. So we got a form group that gives us a label, and then we just wrap that. Okay. So let's take a look at that. We'll just open this up. We'll do new form group. We've got the name. That'll be an input. Title of the task or to do. We don't really need an icon for this, and we'll just v model out new to do dot name. And we'll do the same for description. Alright. So we'll just model this up. Description. Description of the task to complete. And we'll just do description. And then we've got, we'll just use their button component as well. Cool. Let's clean this up a little bit. Actually, let's add some padding to the whole form. So let's do something like p 8. Maybe we add a header here. I've got a built in VText component here. Let's make the size large, say task. And then we add some spacing between these items, space y 8. Alright. So we've got the form. We've got the actual task here. That's great. Alright. So this is a submit type, on our form. Maybe we wanna do at submit dot prevent, and then we're gonna build a method to submit that to do to our Directus API. So submit. Let's just call it add new to do. And let's go in and build this. Right? So we'll call this an async function, add new to do. And what is GitHub Copilot suggesting? We will, await, create item, add to dos, new to do. So we're gonna pass that. And then it is clearing out the items within that new to do so we can add a new one. This looks pretty good. Let's add one final thing. Maybe we want to wrap this in a try catch. We'll error out. And then the last thing that we probably wanna do is actually update the to do list. Right? So, one thing we can do here is wrap this function. Let's say async function, if I can actually spell, fetch to dos. Return data, get items to dos. Actually, let's just swap that. We'll return the data from that. Great. We'll log any errors. And now we're not seeing any to dos because we actually need to call that during the setup of this. So we'll call const to dos equals ref. And I could actually do something like this if we wanted to make it reactive where I just have to do's at the top here. And within this function, I could, to do's dot value, just update those to do's, which let's do that. And here, we'll say, wait. And this works because we are in we're using script setup with inside Nuxt review. So we could do fetch to do's. That will fetch the to do's when it sets up this component. And then when we add a new to do, we're going to fetch those to do's again. So if I go in and now let's test this out. New to do. Do a description here. We hit add to do. K. It did something. What did it actually do? Input types contains is not a function. No idea what we're doing here. Form at submit dot prevent is add new to do. This is a submit button. Got new to do dot name dot description. Why is this not showing? Right? Let's try try it again. Test to do. We'll look at our fetch request description, and it looks like this is okay. Post items to do's, payload status equals false. Right? What's what's the issue here? Why are we running into an error? And you could see because I've logged in as a different user in my application, Directus has also logged me out. So we'll just log back in really quickly. And again, we could see that the to dos are being created, but they are not being assigned to a team. Right? So we got 10 minutes and 30 seconds left to resolve this issue. If we look at our user, we could see that we have the team's array. Right? So we have the ID of that team. We just need to pass that along inside the request. You know, if if I did not have, like, a setup where a one user could be involved in multiple teams, you know, maybe I have a a preset within the API itself to to do that. You know, if if I've got a user that is a part of multiple teams, we have to pass that along, because it's not gonna know which team. So here inside this, we could also do something like this where we have a switch for this or, let's just pick up the team from the user. Right? So we've got the user here inside new to do, new to do dot team. Let's add the user dot, teams. It's just the first item inside the array. So something like this. Alright. Let's see. Test. Test. Test. Test. Test. Does that actually work? Looks like we're not getting any response from API team. Okay. There's the team. Okay. It's because we are the team is not an array. It's just a single value. Okay. Try it again. Test. Test. Add to do. What are we not doing? Right? So the team is actually we're not calling that data at all. New to do dot team equals user dot teams. Is that because that is a value? Let's refresh the page. Just test. To do's. Okay, it's it's not performing our post request. Cannot read values of undefined. So it's probably something like this. Let's just unref that user Dot teams 0. Test this again. To do's is a bad request. Okay. So we're passing something that Teams does not like. The team status is false. Why why is it not liking that at all? Team so if you look at our access control, let's try to diagnose this. We got the to dos. We can create to do's. We got access to create all the fields. And if we just delete some of these other ones, What's going on? Why can't we assign a to do a new to do to team a? Right? We should be able to do that. Test test test test test test. Todos is status is is false. Maybe that's it. Test. Test. It still does not like the fact. Invalid foreign key for the team. Okay. So maybe it should be an array then. Let's test again. Do not have permission to access this. Invalid foreign key for the team to dos. Why are we seeing this particular issue? Right? What are where are we at on the clock? We got 6 minutes. Let's resolve this thing and finish this one up. Right? So let's just leave the status out for now. Why are we not getting the proper response from Directus? Alright. So we go in, we look at our data model for our to dos. We've got a team. That's a mini to 1. If we just were to, like, log this out, let's say we give access to public access for all of our to dos, all the teams. And just to debug this a little bit, if I go into items, I do to dos, I could see that team is just, a single string. That's the UID for that string. And it should be one of these values, like 8bf or ending in a 69e. What are we doing? What are we passing inside that request? Test. Okay. Now I'm logged in as the the user again. Let me clear all the cookies. That could be what's going on. You know, if you're working with Directus in one window, you're using the API in another window, maybe you want to make sure that you're using incognito windows. So when you log in that you're not seeing something outrageous here. Let's clear our cookies out. Oh, and now we can see all the to do's because we are not logged in. And I could set up those permissions. So let's restrict those permissions again. We're just gonna remove all those. Great. And now we log in. We do team a user at example.com, go to password, go back a step, now we've got our to dos. Let's test. Alright. We're still getting the same thing. You don't have permission to access this. That's because it's not an array. We refresh, test, test. Again, invalid foreign key. Right? So what are we actually getting here for the team? If I look at our user, teams dot okay. So we have the user there. I don't understand why we're not able to access the value. Oh, let's add a team. We'll just give it a false string. User let's try user dot value dot teams dot 0. Maybe we just actually console log the the user. The user is right. Let's take a look at that. Alright. So we've got our to dos. We had test to do. Alright. So there's our user. Teams. Alright. Bad requests. Invalid foreign key for team in collection to dos. Something to do with passing the wrong team. So if we take a look at our teams, team a, Why are we getting the wrong value here? So team a should be this particular value. We're sending 5 e. Is that correct? 4e? No. I I don't know why we're where this value is coming from for teams for that specific user. Right? Team a user, do we add that user to a team that is not available? Team a. Oh, duh. That's because we're not actually getting the team user, we are getting the junction collection. So great idea, Brian. Let's go into our config for this and it actually should be the junction collection. So the, we need to access the team from the junction collection instead of the just the okay. Alright. So we can fix this. We'll go into our Nuxt config. Where are our fields for this? User fields, we're going to pick up the teams dot okay. So let's just refresh. Come on. Come on. Let's build. We got 30 seconds left to fix it. Come on, build, baby. Okay. So now within our function, we are going to go in and call instead of teams okay, then we're gonna use teams underscore ID. That should be it. And all these issues probably would have been prevented had we used TypeScript. But now we could see that. And with 54321 on the clock, you could see as I add to dos, boom, it is fetching those and updating them. Wow. So right at the clock, we have finished the multi tenant to do application. What would our next steps for this sort of thing be? Right? One probably hooking up subscriptions and and actually fleshing out this logic. So as far as next steps, let's just discuss those for a few. You know, add subscriptions. I can unstriker through those. We would add our subscriptions using something like Stripe. Clean up all of our data fetching, fetching and handling, and probably extract some of that logic into a middleware. But the major achievement here is being able to create that multi tenant role and permission setup within an hour, so that each user can only see their data. They can only interact with that data. So, great. You know, we took all 60 minutes on this one. The UI doesn't look fantastic, but as far as our back end, pretty much ready to go. I hope you'll join in for another episode of 100 apps 100 hours. Until then, I'll see you around.",[258],"1f07407a-d65f-4c6e-a092-4c02958b0c88",[],{"id":172,"number":131,"show":122,"year":173,"episodes":261},[175,176,177,178,179,180,181,182,183,184,185],{"id":179,"slug":263,"vimeo_id":264,"description":265,"tile":266,"length":267,"resources":8,"people":268,"episode_number":270,"published":271,"title":272,"video_transcript_html":273,"video_transcript_text":274,"content":8,"seo":8,"status":130,"episode_people":275,"recommendations":277,"season":278},"pim","895881145","Join Bryant as he tackles creation of a product information management system to manage and distribute product data to e-commerce channels. He has one hour to build the data model, add some data, and create an integration to automatically push products to Shopify.","b2764b86-d5df-4dc1-90d2-2c7bcdc1462c",65,[269],{"name":199,"url":200},5,"2024-01-08","Mission: Product Information Management","\u003Cp>Speaker 0: Hi. Welcome back to the next episode of 100 Apps 100 Hours where we build some of your favorite apps or die trying in 1 hour or less. So, I am your host, Brian Gillespie, a developer advocate at Directus. And, today we have PIM, which I've only started learning about in the last few months in detail. So PIM stands for product information management, sort of like a CMS for your products.\u003C/p>\u003Cp>But the main problem that you solve with PIM is, having a single source of truth for all of your product data. Imagine that you've got different ecommerce channels like a Shopify store or you're selling on the Amazon Marketplace or walmart.com. Being able to manage all those different products and all the variations and all the colors and images and everything that goes along with selling those products in one place instead of managing it in 3 different places or 5 different places is tremendously valuable. So there's a couple of other tools out there in the space. Pimcore is the name of one of them, SalesLair and Plytix are a few of the ones that I took a look at.\u003C/p>\u003Cp>And when it comes to PIN, before we dive in to actually building this, let's just take a look at at one of these solutions. So we've got a single source of truth where we find some screenshots. Can we find some screenshots? Kind of combines, all of my products, all of my assets, and you can generate things like catalogs or product sheets and sync this product data to those individual channels, which is the the biggest part or the, to me, where the real value is. I manage all of this in one place and sync it elsewhere.\u003C/p>\u003Cp>So we are going to build APEM in 1 hour or less. Right? If you've caught some of the episodes other episodes you already know the rules of the game in that there are 60 minutes to plan and build no more no less, and the other rules are there are no rules use whatever you have at your disposal which I will take plenty of advantage of in this specific episode. Alright, so with that let's open up our timer over here on the left and we'll get started. So the first thing that I like to do anytime I'm building an app is just a little bit of planning.\u003C/p>\u003Cp>So what's the functionality? What does our data model potentially look like on the back end? I like to work back end first just because it's so much nicer building a front end if I need 1 in, with actual live data. So what are the features or the functionality that we need out of a PIM? We need to be able to store all of our, store and manage our product data.\u003C/p>\u003Cp>Store and manage, all product assets, images, video, spec sheets, etcetera. Let's make this a nice list. I'm a recovering designer. I can't ever get that out of me. And then we need to be able to store and manage, we need to control product variations.\u003C/p>\u003Cp>Product variations like color and size, you know, other things like that. Then we want to be able to sync that data with other, with ecommerce. So let's call that Shopify in this case. And, you know, maybe even have a simple product catalog, where we could show off those products. Good enough.\u003C/p>\u003Cp>That looks like a pretty good set of base functionality for a PIM. We've got the single source of truth for that. Now let's dive into what our specific data model might look like. So we've got our products, and I'll do the lowercase version here. No fill.\u003C/p>\u003Cp>We could keep this purple color though. I like that. Alright, so we've got our products, we've got variants of those products, so maybe product variants. I can imagine this is like the specific color, size, g10, things like that. We got a name, assets, other data.\u003C/p>\u003Cp>Very lovely. What else are we going to have? Probably some categories for our products. Categories or collections. Maybe those are even recursive.\u003C/p>\u003Cp>What else do we have? We have assets, which our actual back end that we're using today, Directus, will take care of for us. What else is gonna be inside our data model? This looks like a pretty good start. Right?\u003C/p>\u003Cp>So let's actually dive in. We're about 3 minutes into it. Hate to spend too much time planning. Alright. So we will log into our back end, Directus, today.\u003C/p>\u003Cp>I've got a blank instance set up. This is using the cloud service. So this is actually running on Directus cloud and not locally because we do wanna sync this data, and, I don't wanna really struggle with cores issues or anything else, in local development trying to communicate with Shopify or other systems. So you can see I've got a blank instance, and it might be helpful just to leave this up side by side. Keep an eye on the clock.\u003C/p>\u003Cp>So I've got my blank instance of Directus. What I love about it is how easy it is to go in and build out our data model. So let's just start by creating a new collection. We've got Products and we're gonna give this a generated UUID for the primary key field. We'll add in a status, a date created, user updated, just some of the system fields, that are, like, built in utilities, basically.\u003C/p>\u003Cp>You know, we wanna keep track of what user updated, who, and when, just so we've got that information. Alright. So as far as products, we've, what, got a name for the product. We probably have a SKU number, SKU number, SKU part number. You know, you could have a couple of different items here.\u003C/p>\u003Cp>We've certainly got a description of the product, which we'll use our WYSIWYG editor inside Directus for. That way we can embed rich content. Sounds great. What else are we gonna have on the individual product? You know, a like, the price information may live on the the product variance there, so I'll keep it there.\u003C/p>\u003Cp>We're gonna have assets for this specific product as well. So I can go in and we'll create a new field and we'll use the relational files interface here which will actually create a junction table in our SQL database for us. So we'll do the product let's call this product Assets. Sounds great. And I could go into the advanced editor just to see what is happening behind the scenes.\u003C/p>\u003Cp>So we've got one collection called Products. We've got a related collection called Directus Files that is a built in system collection within Directus and then it automatically creates this junction table for us. Now if I wanted to adjust the name of the fields or control what this actual collection is named, I could turn off autofill and make that available. You know, and maybe I do want to add the concerning field or the reverse field to our products and we'll do a sort field just so I could control the primary images. Everything else looks great.\u003C/p>\u003Cp>We'll just save this, and now we've got some product data, right, or a product model where we can start uploading our data. Hey, this is a shirt and the SKU number is 1234. This is the best shirt ever. And we could even go in and upload an asset. We're probably gonna do a little bit of copyright infringement here.\u003C/p>\u003Cp>Just copy the image URL. We can upload this and I can do that by URL. Cannot fetch file from URL. Okay. Service unavailable.\u003C/p>\u003Cp>So I guess I'm going to have to download this image and then we'll just upload. So we get a pretty good idea of our basic functionality here. So now we've got our assets, and let's say I did want to browse this like a potential catalog. I can go into Directus, I could change this to the card view from our information sidebar and then I would just adjust my image source here. It looks like we don't support many to many fields there.\u003C/p>\u003Cp>So maybe we go back in and add a, like, a featured image. Featured image. Great. Alright. So this is just a single image that we want.\u003C/p>\u003Cp>So we'll go into that specific product, and I could pick this same image from the database. Alright. Great. So now we've got the featured image, we've got a name for the product, we've got our SKU number, and I can even go in and control how the image is cropped or not. So great.\u003C/p>\u003Cp>Now we've kind of got this, catalog looking module happening. Let's go in and finish fleshing out our data model. So I'll just clean this up a little bit. We've got our product variance and we'll use the generated UUID. Same thing, I will go in and add all these separate fields.\u003C/p>\u003Cp>Maybe we do have a sort field on the variant in case one variant takes priority over the other. Yeah. Kinda hit or miss. Alright. So we've got, our product variants.\u003C/p>\u003Cp>What we are going to do is create a many to one relationship back to our parent product. So this will be the product, and we're gonna choose the products collection. And this is a mini to one relationship. So I'm gonna go into the advanced field settings here. We'll just create a new, a one to many on the other side of the equation back on our products table for product variance.\u003C/p>\u003Cp>So now what I've done, in effect, is link these two collections or these two tables in our database together so that we can query and keep track of them. So now let's do what do we want to add for our other variant data, you know, so we've got color, that's just a string, we've got, size, etcetera. Great. We want to control a price here. You know, maybe we got that g ten, you know, unique identifier.\u003C/p>\u003Cp>What is that? The global trade identification number? Something like that. Let's go in and add a decimal. Actually, that'd be that'd be a float.\u003C/p>\u003Cp>And then we'll do maybe just 2 decimal places. Let's call this price. Great. Alright. So now we've got some items on our variants.\u003C/p>\u003Cp>Let's go back and actually look at how this works. So I go back into our product and we can see we've got our variants here and if I wanted to create a new variant, I can. Let's call this red, we'll call it small. The GTIN is 666 and the price for this is $25. We could go in and create another variant.\u003C/p>\u003Cp>This is red, this is medium, you know, 777, and the price is $16. Whatever. I can go in and actually set up our like what fields are displayed here. This doesn't look very great just to see the UUIDs. So let's sort that out, right.\u003C/p>\u003Cp>And we'll make this just a little bit bigger as well. Alright, so we go back into our data model, we go to our product variants, we look for our interface and we have this, control over do we want a list, do we want a table. Let's use the table in this case and we will add our color, size, maybe we'll do GTIN and price combinations. Now you also may have, like a variant that has a different image as well. So we could potentially add that as well.\u003C/p>\u003Cp>We'll just keep it simple for now and for the related fields, let's show the color. We'll add just like a little label for it so we know what it is, we'll add the size and we'll add size here. So let's see how that looks inside our PIM and great. Now we can see here's the different variants, there's the pricing, which, you know, maybe we wanna format that a little more, but great. And we can click on those to edit that information all that we need.\u003C/p>\u003Cp>Alright, now let's build out our categories, right? This is the only thing remaining on our data model. We want to build out categories for our different products. So we have a category, we'll use the generated UUID again, and not sure we need like a status on these, but maybe we do wanna sort and cool. Alright, so we've got a category name.\u003C/p>\u003Cp>Probably a short description for category. You know, we keep kinda splitting hairs which you on what detail you you need for something like this. If we look at, let's let's look at something like Pimcore. What what do they have available on their website? This looks really kinda messy to me.\u003C/p>\u003Cp>So we've got, different objects. We've got media. You know, I don't really know for certain how others set up their data model for this, but to me this is how I would do it. Now I could go in and have something like another classification for this. We could use, like, tags as a different type of taxonomy, a different way to organize the data.\u003C/p>\u003Cp>But I also have, you know, probably subcategories and parent categories. So Directus makes doing a recursive relationship like that very, very easy, where I can go in and do a many to 1 or a one to many relationship. So I'll just do one to many. We'll call this the, these are gonna be subcategories. Let's just do let's call let's do it the reverse.\u003C/p>\u003Cp>So this will be the parent category. We'll use the categories collection. So we're creating a relationship, a recursive relationship, and we're gonna do the subcategories. So we've got parent category which will be a single parent, then we have subcategories. Great.\u003C/p>\u003Cp>So what effect does that give us? Right? If I go into categories so let's create a new category we'll call it apparel. This is, let's call it actually, well, like outerwear or something like that. Great.\u003C/p>\u003Cp>Hoodies, jackets, and such. And we don't have a parent category or a subcategory yet, so let's just create a subcategory like hoodies. Hoodies. Great. We'll save that and now we can see we've got subcategories and, you know, potentially nest these.\u003C/p>\u003Cp>If I go back to our product, now we want to create a relationship with that category. So in this case, typically, a product may only have a single category. I I've seen some of these systems that will have, like, smart collections, which I think is a a really neat way to model it. And I think even Shopify, so I've got a a Shopify store set up here. Let's let's just take a look at how they handle it.\u003C/p>\u003Cp>If this can be in a multiple categories or no? So it looks just like a single category. You can set up different collections for it. So we'll keep this to be a mini to 1. So we've got a category and our related collection will be categories.\u003C/p>\u003Cp>And for our display template, maybe we just want to show the name of that category. Now, one of the other things I can do is control the interface to how the related values are displayed, just to make sure this looks how we want. So now if I go into our product, we can select the category hoodies or outerwear specifically. Great. Cool.\u003C/p>\u003Cp>Now, let's say what functionality, let's go back here. I always get ahead of myself. So now we've got a way to store and manage all of our product data. We've got a way to store all of our product assets, right? So I could go in and upload.\u003C/p>\u003Cp>Let's say I've got a PDF here. I could go in and upload this PDF. If I've got a spreadsheet, I could certainly do that as well. You know, I've got a CSV or whatever data that I've got, I can upload that and store it within this product. But, you know, what fun is it having these products in isolation?\u003C/p>\u003Cp>We wanna be able to use these specific products. So out of the box, because we've set up this data model, we know the structure and everything, Directus gives us the ability to well, it just gives us REST and GraphQL APIs. So I could go in and do something like this where we've got items. Products. I'm getting an error message because I haven't set up permissions, but let's imagine we wanted to allow anybody to access this product information.\u003C/p>\u003Cp>I can go to our setup, We'll go to access control and we'll go to our public role, which just controls what information is publicly available. And because none of this is secret at this point, let's go in and try it now. So if I hit refresh, we go to our products, we can see our product data. So there's the shirt, there's the SKU number, there's the description, we've got a featured image, UUID, And when it comes to our variants, we can't I can't see that information. That sucks.\u003C/p>\u003Cp>How do we fix this? I can actually do something like this, which is one of my favorite features inside Directus that I can query the related collections or the related fields from a single API call. So it's very GraphQL like even though this is REST based, and I, you know, there's a GraphQL API. I'm not a super huge fan. You may be.\u003C/p>\u003Cp>No sweat. So I could do something. We'll give, like, we'll use a wildcard here for all the root level fields. And then let's go in and do something like this where we have product variance and then we'll get all of the brute level fields for that. So as soon as I do that, now you can see I start to see that data that we want.\u003C/p>\u003Cp>And maybe I don't need all of this. Maybe we just need the ID product_variance dot color. I could get a little more sophisticated and copy paste dot size dot price dot g10. Alright. And now that will only return the data that I'm interested in which is nice prevents over fetching keeps things really fast when we are sending a lot of data over the network.\u003C/p>\u003Cp>Alright. So we've got the ability to store and manage all of our product assets. Let's control our product variations. How do we sync this data with an ecommerce system? Right?\u003C/p>\u003Cp>We're managing all of this in a single location. Maybe I've got Shopify, which is what we're gonna use. Maybe I've got an Amazon storefront setup. Maybe we've got 3 or 4 other things going on. Right?\u003C/p>\u003Cp>So how do we sync this information this data over? So I'm gonna go to our store and this is just the the Shopify demo store that I've set up. None of these products or anything that I created. Right? It looks like this is set up for snowboarding, which I tried once or twice, wasn't very good at it.\u003C/p>\u003Cp>But I'm gonna go into the settings of this and we're gonna try to find how to, get access to the API here. Let's look and see domains. Maybe it's here inside the apps. Okay. So we'll look for develop apps.\u003C/p>\u003Cp>We're gonna create an app that can connect with this storefront. Let's just take a look at it and see what it actually looks like on the front end. We got a password protection right now. We're gonna call this the Directus Shopify sync, maybe? We'll create this app inside the Shopify store.\u003C/p>\u003Cp>Alright. So it looks like the next thing that we'll need to do, we're probably gonna be using the admin API because we're not building a shopping experience. We're trying to send product data to Shopify, so that we can have that experience inside Shopify. So let's configure our access scopes. It's probably gonna be around products.\u003C/p>\u003Cp>And I'm not sure exactly which one of these I need. Product listings, product feeds, viewer manage products, let's go with that. We see the webhook subscriptions. We'll come back to webhooks. Let's just save that.\u003C/p>\u003Cp>What else do we need? We need some actual credentials. So we need an access token, a way to get that data. Use your client secret to verify incoming webhooks. Let's go ahead and install this app.\u003C/p>\u003Cp>This will give this app access to our storefront data. And we're only gonna reveal this token once and boom, we don't need it any longer. Not sure exactly how to set up the webhooks. Is that part of this? Start using the admin API.\u003C/p>\u003Cp>Do you have to create webhooks through the through the API? I don't know. If your app creates a webhook subscription oh, okay. So it looks like you do have to create webhooks using the API. Yeah.\u003C/p>\u003Cp>Alright. We'll come back to that. No worries. Alright. So now I've got this product information inside our account, I want to send it over to Shopify.\u003C/p>\u003Cp>How can I do that? Well, I could probably write some kind of custom app or I could just go in and use the flows, the the automation builder inside Directus. We're looking pretty good on time. We're at 36 minutes. So I'm just gonna make this full screen so we can dive in a little deeper.\u003C/p>\u003Cp>Let's call this send products to Shopify. Now how are we gonna trigger this? There's multiple ways to set up triggers. So I could manually trigger this. I could set it up on a cron job, so a regular interval.\u003C/p>\u003Cp>For now, I'm just gonna manually trigger this and we'll go we'll trigger this on our products section and maybe we'll require confirmation. Are you sure? Yes, we're sure. So the location here allows me to control where I can actually trigger this. So I'm just gonna trigger this on the collection page only.\u003C/p>\u003Cp>No. I could do both, you know, I could potentially send one product or maybe I want to send all of the products. We'll just keep that set up. Alright. So now let's go in.\u003C/p>\u003Cp>We'll save this flow and if we go back to our products page on the right hand side, I could see the option to send this to Shopify. So check an item. Are you sure? Yes, we'll run a flow. And if I go back to our flow, I refresh and I don't see my logs.\u003C/p>\u003Cp>Where are the logs? I ran this flow. Where is it? Let's make it not require a selection. Not sure if something happened here.\u003C/p>\u003Cp>What's what's going on? So we'll go to our flow. We'll hit send products to Shopify. Go to flows. Okay.\u003C/p>\u003Cp>Now they're showing up and we can see kind of the data that is being triggered here. So just collections, products. Great. Cool. So now, how do we get this actual data, right?\u003C/p>\u003Cp>We will go in and read the data. So let's just call it read products. That's gonna be the name of this operation. We're gonna select all the products from the collection products and then I could, like, filter this down if I wanted to. You know, maybe I I just leave it all for now.\u003C/p>\u003Cp>We're gonna select all the IDs and save it. And then once we read that data, we want to send to Shopify. Great. We'll, actually use the webhook HTTP request. How are we going to send the data?\u003C/p>\u003Cp>It's probably gonna be a post method. We're gonna go let's go back to Shopify really quickly. So we've got our storefront API documentation. Zoom in a little bit. Do we want the storefront?\u003C/p>\u003Cp>I don't think we do. So many tabs. So we go to admin. Oh, developer tools, API libraries. Oh, boy.\u003C/p>\u003Cp>Getting into the thick of it now. Let's go back to where's our actual store? Right. Explore, start using the admin API. Okay.\u003C/p>\u003Cp>Very confusing documentation here. Alright. So we'll go to our REST API reference. We'll look for products is probably what we want. We want to create a product.\u003C/p>\u003Cp>So there's what the product resource looks like. Here's our create a product, and there's our URL. So what does the base URL look like? Authentication. Okay.\u003C/p>\u003Cp>So it looks like the URL structure here is the Shopify unique shop ID or the shop slug, I'm assuming. My shopify.com/admin/api/ apiversion/resource. So our base URL setup so if we go back, will look something like this. Let's just pick up this actual URL. 100 apps, 100 hours, nice slugified version.\u003C/p>\u003Cp>Myshopifiedot com/ what was it? Real genius here. Okay. Let's go half screen. Myshopify.com/admin/api/ what?\u003C/p>\u003Cp>2023 dash 10 dash products dot JSON. Okay. So there's our URL. Great. Now I've got that access token.\u003C/p>\u003Cp>Authentication. Looks like we have to pass that as a header as well. So we'll go into direct us. We'll add some headers. Got content dash type.\u003C/p>\u003Cp>That's gonna be application slash JSON. And then we've got x Shopify access token. And then we've got this special token that you're not gonna look at, you're not gonna read, you're not gonna try to use. Alright. So now we can send a request.\u003C/p>\u003Cp>So we go down. We're looking for products again. We're gonna create a product inside Shopify. Alright. So what are the details we need for our products?\u003C/p>\u003Cp>We got a product dot title, body HTML, that's our description. We've got a product vendor, that's one thing we forgot inside our pin that we could add. We've got the product status. So, let's go in and just do something really simple. We'll do products.\u003C/p>\u003Cp>Okay. So there's our product. Then we're gonna have a title for the product, what's that gonna be? Let's take a step back here. I'm just gonna save this.\u003C/p>\u003Cp>I need to see what actual data is coming from this call here before we get carried away. Create a new product. Can you create is there an endpoint for creating multiple products? That's what I'm interested in. Create a new product.\u003C/p>\u003Cp>Retrieve a list of products. Looks like we're just going to be able to create a single product at a time. So we may have to break this apart and do 2 separate flows. Imagine that I had another product. We'll call it test product.\u003C/p>\u003Cp>No. Let's not do that. Let's call it a hoodie. So it is hoodie 123. Let's go to unsplash maybe.\u003C/p>\u003Cp>Do a hoodie. And this looks like a hoodie with a statement so we'll just copy paste that. Okay. We'll import the file from the URL. Boom.\u003C/p>\u003Cp>We got a featured image. Put a description. Great. Alright. So now we get 2 products.\u003C/p>\u003Cp>We're just gonna select these products. We're gonna send the well, what happens if we don't select? We could still run this flow. We'll We'll take a look. We'll send these products down to our flow.\u003C/p>\u003Cp>And let's just look at the log. Right? So now I'm getting an array of products. Okay. And maybe we wanna do we're gonna break this apart because we're gonna have to send a single product to Shopify.\u003C/p>\u003Cp>But before we do that, I want to format products. Right? So we'll take our product. We'll run the format function. So Directus allows you to run arbitrary JavaScript inside the flows, which is really nice for stuff like this.\u003C/p>\u003Cp>We've got the data, so our products are gonna be constant products equals data dot read products. I think that's gonna be it. And then we have some formatting logic here. Right. Just thinking out loud.\u003C/p>\u003Cp>So now I'm just gonna save this really quick. And one of the other things that I like to do when I'm working with these flows is just copy the data I'm getting back right here, and I'm gonna put it inside my code editor. I've got a starter project here just in case we needed it. So now I've got the data structure. And if I look at Shopify gosh.\u003C/p>\u003Cp>We've got, like, 35 different tabs going on. That's the fun of doing these live is actually trying to figure out how to all how to keep it all sane on one screen. Admin, API. Alright. So we go to the admin API, Rest admin API.\u003C/p>\u003Cp>So we go to our products. What things do we need to pass to our products at a most at a minimum just to actually create these products. So here we go. Here's a new draft product type of setup. Creates a new draft product.\u003C/p>\u003Cp>And can we do let's just make this even more complicated. Right? We'll do like a split screen? Somebody still needs to teach me to properly arc. Alright.\u003C/p>\u003Cp>So I've got Directus, got our product. Okay. We got like 30 things going on here. A lot of fun. Alright.\u003C/p>\u003Cp>So, let's run a format script. We've got our data over here that is coming from our Directus database. Let's go in and add our logic here. We got products. Let's do Shopify products dot map.\u003C/p>\u003Cp>Oh, no. Shopify products equals products dot map product. Yeah. Products. And then we'll return something.\u003C/p>\u003Cp>We got our function there. And then at the end of this, we're gonna return Shopify products. So we're just gonna return the array of products we want from Shopify. Alright. So we got the products and then we're gonna return a title for the product which is gonna be the product dot what name.\u003C/p>\u003Cp>We need a comma there. Then we've got the body_html, that's gonna be product dot description. We've got product dot vendor. We don't have that product dot type. We could fetch that, and then for now, let's just try this and see how far that actually gets us.\u003C/p>\u003Cp>Product dot name. We're gonna return our Shopify products. Let's go back in and I'll just make this full screen. Do products. I will look for our sidebar.\u003C/p>\u003Cp>Where are you sidebar? There you are, mister sidebar. We'll send the products to Shopify. Let's just test our flow and see what came out the other side. Right.\u003C/p>\u003Cp>So this looks good. This is our format that we're gonna send to Shopify. I'm gonna take just my my headers that I've got here. Let's just stuff those in this, take my URL that we so carefully constructed. And, I'm just gonna leave that.\u003C/p>\u003Cp>We're gonna create a new flow. So we're gonna go in and create Shopify product and this will be triggered from that other flow, so triggered by another flow. We'll leave the response body blank and here we're just gonna do the HTTP request that we just did. So I'm gonna paste in that. I've got my headers.\u003C/p>\u003Cp>Still stuck on the clipboard somewhere. I'll edit raw value and now we've got that. And here, I think it is just going to be something like this where we do actually, hit save. We'll just disconnect this for now. Hit save.\u003C/p>\u003Cp>Go back to our other flow and trigger that flow, and we'll just see what trigger Shopify, create. And Directus will create a key for you, you can also rename this key, but this key, all the data that gets generated in this operation gets appended to the flow underneath this specific key. Let's just take a quick peek at time. How are we doing? We've got 20 minutes left.\u003C/p>\u003Cp>Great. And then we're just gonna trigger this flow. So we'll select the Shopify flow. It's okay to create these in parallel and then for our payload, we're gonna do simply this. We've got the double curly brackets.\u003C/p>\u003Cp>We've got the data. No. What's the name of that operation? Format product. Is that what the key is?\u003C/p>\u003Cp>We're just gonna use this key. So we're gonna trigger the flow and the data from that, format product, we're gonna pass that to that other flow. Alright. So fingers crossed, stay with me here, we are building cool stuff. So now we're gonna send these products to Shopify, running that flow.\u003C/p>\u003Cp>And we can see here we triggered this. We got back null null, this is what we sent to that. And if we go to our other flow, where we're actually creating, we can see that this ran twice, which is the what we actually want. We wanna see the the payload that we got was the product. So we're just gonna pick up the trigger.\u003C/p>\u003Cp>Payload and that's what we're gonna send. Great. Amazing. So, in our request body here, we're gonna do something like this where we've got, quotations, double curly bracket, trigger dot payload and trigger is, prefixed with a dollar sign just because it's a unique, special key to get the trigger data. And fingers crossed.\u003C/p>\u003Cp>Right? Can we send this data to Shopify? Can we build a working PIM in an hour? Let's test it out. We're gonna run I I don't think it matters if I select these or not.\u003C/p>\u003Cp>Let's go in. We'll see. We have our send products to Shopify. That ran. Hopefully triggered this other flow that we have.\u003C/p>\u003Cp>We got a Shopify product. Bad request. By accessing the Shopify, you agree to parameters are missing or invalid. Okay. So we're missing some parameters.\u003C/p>\u003Cp>Let's take a look at the actual data that we sent. Payload, got the product resource, body HTML, supports HTML formatting, product status dot draft, Shopify access token. It looks like the body is not defined. So, I think I goofed up and that and maybe we're just sending the trigger. Let's take a look.\u003C/p>\u003Cp>Did I get it wrong? Do we just need to send the trigger? So, let's make this more fun. Right. Let's go to our Shopify store.\u003C/p>\u003Cp>Alright. We're gonna close this guy. Alright. I'm on the products. I'm gonna hit refresh.\u003C/p>\u003Cp>Dun dun duh. I don't I don't see any products in here. Alright. So, let's go back. Obviously, I goofed somewhere again.\u003C/p>\u003Cp>And we're definitely in danger of running out of time here. We got a bad request, missing parameters. Okay. So there's the body, product title, product dot HTML. What are we missing?\u003C/p>\u003Cp>100 apps, 100 hours. We've got our access token. We've got our content type. Required parameter. Did I spell something wrong?\u003C/p>\u003Cp>This is always the danger of working against the clock like this. What is this issue going to be? Request body is product. Alright. So product title body HTML.\u003C/p>\u003Cp>I don't see where this is going wrong. Shopify access token. Bad request. Let's take a look at our app. Do we have the wrong scopes or something?\u003C/p>\u003Cp>Let's just try one quick tweak. Go back to our send products for our format product. Let's just leave out the title. Right. So we'll just comment this out.\u003C/p>\u003Cp>Return Shopify products. Send products to Shopify, flows, bad request, product underscore. Is it the way that it is actually sending this data? Sometimes if we do something like this, where we got trigger. Boom boom, Ron.\u003C/p>\u003Cp>Created. Boom. Alright. So dramatic reveal time. Dun dun dun.\u003C/p>\u003Cp>We have sent this data to Shopify. Did we? Where do we go? Okay. So we've got a hoodie, we've got a shirt.\u003C/p>\u003Cp>There they are. Boom. Now they are in our system. That is how it's done, ladies and gentlemen. Where where we at on time?\u003C/p>\u003Cp>We got 13 minutes left. We have managed to sync product data. Let's let's just take this one step further if we can, and let's create a product with let's at least get the images over there. Right? So on our product resource, we've got a list of product images, create a new product image, create a product image using a source URL that will be downloaded to Shopify.\u003C/p>\u003Cp>Okay. So it looks like you can you may be able to pass these, and I'll just go in and very briefly let's just delete these products. Delete these products. Can't be undone. Those are deleted.\u003C/p>\u003Cp>Those are no longer in the system. Let's adjust our payload here. So we're gonna go back to the formatting option. We'll comment that back in. We can have that now and then on the Shopify side, we've got okay.\u003C/p>\u003Cp>So we got a product dot images. This will be images and then we've got an array And then what? Array of objects, and then we just got a source attribute. So the source if we look at our code again, Right. We've got our featured image.\u003C/p>\u003Cp>Let's just pluck that for now And, we're gonna do something like this where we've got the source is gonna be, HTTPS direct us app slash assets slash what was the name of that? Featured underscore image. Featured. Oop. Gotta do our little squirrellys featured image.\u003C/p>\u003Cp>Close that off. Should be good. The last piece of the puzzle for this is going to be enabling permissions for the actual files inside Directus as well, Because Shopify needs to access that. So 2 ways I could do it. I can add an access token, when I'm sending that call to Shopify Or in this case, I'm just gonna go in and we'll keep this really simple.\u003C/p>\u003Cp>Under direct as files, we'll do all access. And we're just going to send and cross our fingers hoping that this flow actually works. We could do the dramatic reveal inside Shopify. Right? I broke something trying to send the images over.\u003C/p>\u003Cp>Where did you go? Where did you go? Alright. What broke? Right?\u003C/p>\u003Cp>Clearly, we broke something. Great Shopify product. What do we send? Product title hoodie. Do we actually break something inside the I don't see my logs.\u003C/p>\u003Cp>4 to 5 minutes ago, that's not correct. Run Shopify flows. Go into our flow. Less than a minute ago, featured image is not defined. Okay.\u003C/p>\u003Cp>So we did not have one with a featured image, and that broke. What did I do wrong? Again, always fun on the clock. I guess you have to actually use product dot featured image. Right?\u003C/p>\u003Cp>Featured image. That would make a ton of sense. Too much sense actually. Alright. Last time.\u003C/p>\u003Cp>Send products to Shopify. Did it work? Let's test our flow. Create Shopify product. Created.\u003C/p>\u003Cp>Good looking. Alright. So if we open up Shopify, we go to our products, we can see our hoodie. Here is our actual data, so there's the image that we've got, there's the shirt that we loaded up, and this is all coming from our pin, our back end, our single source of truth. So that is how it's done, ladies and gentlemen.\u003C/p>\u003Cp>We are syncing with Shopify. Last on our list, can we get this in 8 minutes? I have no idea. We've got a simple product catalog. Now, I don't think we could do this in 8 minutes, but let's give it a shot, right?\u003C/p>\u003Cp>We want to display all of our products on the front end of our website, so people can browse those. So because there are no rules, I am a huge fan of Tailwind UI and Tailwind CSS in general. They've got some great looking ecommerce components that we may just repurpose for this, like, here's a nice product list. And I've got a Nuxt starter application sitting here just ready to be used. So let's see if we can actually get this product information displayed and rendering on a product catalog in just a couple minutes.\u003C/p>\u003Cp>Alright, so we're gonna just forget Shopify for now. Let's simplify. Alright. This looks pretty good. This is a with border grid.\u003C/p>\u003Cp>We've got a view component here. We've got our products. Let's just copy this template. Alright, so we're gonna go to our index page. We probably got some script action that we'll do there.\u003C/p>\u003Cp>For now, I'm gonna if we just copy their products. Right. Constant products. Not gonna look great, and they've got an icon. So we'll do the star.\u003C/p>\u003Cp>Let's just omit the products ratings for now. Right? Just comment those up. Run this. If I look at local host, say now we've got got something cooking here.\u003C/p>\u003Cp>K. And now, let's actually fetch these products from Directus. We wanna do this live. Right? Going to do what, we got our products.\u003C/p>\u003Cp>Is it equal to await use directus? I've got a just a composable here to fetch this data using the rectus SDK. We're going to read items and that's gonna be products. And is this actually gonna be all we really need? Usually, when I am developing, I often tend to do stuff like this where I will just render out the data inside the browser just to make sure we're seeing we're getting the stuff that we need.\u003C/p>\u003Cp>Item/products failed. Oh, wait. Use direct us, read items, products. Why is this not working correctly? Well, that would be because I didn't actually set up the directus URL properly.\u003C/p>\u003Cp>We are at 5 minutes, so I'm gonna load my PIM URL here. That is my Directus base URL. The server token, probably not even needed in this setup, but, we'll just reload. Let's fire back up the dev server. Let's see if we can get this data rendering from Directus.\u003C/p>\u003Cp>Boom. So there's our actual data. Right? We no longer need this. Let's uncomment.\u003C/p>\u003Cp>And just looking at this data, we've got the v4productandproducts. We wanna render the product dot id, we'll have that information. The image source, because we can use Directus as an image provider it will transform assets for you. I'm just going to use the NUTS image tag which is already set up in this. We will use product dot featured image.\u003C/p>\u003Cp>We'll just leave alt text blank for now because there is there's none. We haven't got that set up. We got product dothref. We don't have one of those currently, but we could do slash products slash what? Product dot ID.\u003C/p>\u003Cp>That would be the probably the URL. We got a product dot name, we got a product dot price. What else? I wanna fix the template structure here and then maybe we want to render out, div. We'll do like a tailwindpros class and give this v html.\u003C/p>\u003Cp>We'll render the product description. Input must be a string received undefined. So if there let's make sure oh, forgot the item there. Boom. So there we have it.\u003C/p>\u003Cp>We have got our items rendered out. You know, we could continue fleshing this out with the help of Tailwind into a catalog. But now we have done several things, Right? Let's recap with our final two and a half minutes. We have created a PIM system that can store and manage all of our product data, store all of our product assets, control all of our product variations, we can sync that data with Shopify, and we can have a simple catalog.\u003C/p>\u003Cp>But, Bryant, you forgot one thing, our translations for this data. What if I want to have it in French or German or, Canadian? Bad bad joke for all all of our Canadian followers. Apologies there. So with a minute 30, how can we set up, translations for this?\u003C/p>\u003Cp>Right? How can I control other languages? So I'm gonna go into our products. The first thing I'm gonna do is just create a languages collection. We've got a language code.\u003C/p>\u003Cp>Alright. We've got a title or name of the language. Alright. And let's create 2 languages. We have EN, US, English.\u003C/p>\u003Cp>Are we gonna do it 55 seconds? Let's just say fr for fridge. I think that's the code. No idea. We'll go back to our product.\u003C/p>\u003Cp>Let's go in and there's a special translations interface within Directus. Clock is ticking, Brian. So we got the code, we got the direction field, that would probably be right to left. Use current language, we'll save this And then inside our product translations, we would go in and do, name, We would do a description. And where we at on time?\u003C/p>\u003Cp>10 seconds. Brian, can you do it? 10, 9, 8, 7. How do we add translations for this? We can do English name.\u003C/p>\u003Cp>So close. We hit the 60 seconds. I'm just going to finish this or the 60 minutes. We can do the French translation here and save it. So, with Directus we can manage all of these different things.\u003C/p>\u003Cp>We hit time there, that felt really good though. We can manage translations or multilingual content. Alright. So that was a fun one. Multilingual content.\u003C/p>\u003Cp>Really enjoyed this one. Building a PIM in 60 minutes or less. There's obviously more to this, but my next steps would be to flesh out that sync engine a little bit, so that if we made updates inside Directus, I could send those over to Shopify. I could even use the Shopify webhooks in this case, if I made a change to that information in Shopify to sync it back into Directus. I would also probably go in and flesh out the translations interface just a little bit more.\u003C/p>\u003Cp>So that I have all the necessary fields. You know, and I may even have specific fields that we want to add to Shopify versus something like Amazon. So we could go in and do, you know, some more different some different channels that we wanna send this data to. But for 60 minutes, amazing to see what you can actually build with Directus. Stay tuned for the next episode.\u003C/p>\u003Cp>I'm sure we'll have a good one and you'll get to see me hack and slash and burn my way through another product.\u003C/p>","Hi. Welcome back to the next episode of 100 Apps 100 Hours where we build some of your favorite apps or die trying in 1 hour or less. So, I am your host, Brian Gillespie, a developer advocate at Directus. And, today we have PIM, which I've only started learning about in the last few months in detail. So PIM stands for product information management, sort of like a CMS for your products. But the main problem that you solve with PIM is, having a single source of truth for all of your product data. Imagine that you've got different ecommerce channels like a Shopify store or you're selling on the Amazon Marketplace or walmart.com. Being able to manage all those different products and all the variations and all the colors and images and everything that goes along with selling those products in one place instead of managing it in 3 different places or 5 different places is tremendously valuable. So there's a couple of other tools out there in the space. Pimcore is the name of one of them, SalesLair and Plytix are a few of the ones that I took a look at. And when it comes to PIN, before we dive in to actually building this, let's just take a look at at one of these solutions. So we've got a single source of truth where we find some screenshots. Can we find some screenshots? Kind of combines, all of my products, all of my assets, and you can generate things like catalogs or product sheets and sync this product data to those individual channels, which is the the biggest part or the, to me, where the real value is. I manage all of this in one place and sync it elsewhere. So we are going to build APEM in 1 hour or less. Right? If you've caught some of the episodes other episodes you already know the rules of the game in that there are 60 minutes to plan and build no more no less, and the other rules are there are no rules use whatever you have at your disposal which I will take plenty of advantage of in this specific episode. Alright, so with that let's open up our timer over here on the left and we'll get started. So the first thing that I like to do anytime I'm building an app is just a little bit of planning. So what's the functionality? What does our data model potentially look like on the back end? I like to work back end first just because it's so much nicer building a front end if I need 1 in, with actual live data. So what are the features or the functionality that we need out of a PIM? We need to be able to store all of our, store and manage our product data. Store and manage, all product assets, images, video, spec sheets, etcetera. Let's make this a nice list. I'm a recovering designer. I can't ever get that out of me. And then we need to be able to store and manage, we need to control product variations. Product variations like color and size, you know, other things like that. Then we want to be able to sync that data with other, with ecommerce. So let's call that Shopify in this case. And, you know, maybe even have a simple product catalog, where we could show off those products. Good enough. That looks like a pretty good set of base functionality for a PIM. We've got the single source of truth for that. Now let's dive into what our specific data model might look like. So we've got our products, and I'll do the lowercase version here. No fill. We could keep this purple color though. I like that. Alright, so we've got our products, we've got variants of those products, so maybe product variants. I can imagine this is like the specific color, size, g10, things like that. We got a name, assets, other data. Very lovely. What else are we going to have? Probably some categories for our products. Categories or collections. Maybe those are even recursive. What else do we have? We have assets, which our actual back end that we're using today, Directus, will take care of for us. What else is gonna be inside our data model? This looks like a pretty good start. Right? So let's actually dive in. We're about 3 minutes into it. Hate to spend too much time planning. Alright. So we will log into our back end, Directus, today. I've got a blank instance set up. This is using the cloud service. So this is actually running on Directus cloud and not locally because we do wanna sync this data, and, I don't wanna really struggle with cores issues or anything else, in local development trying to communicate with Shopify or other systems. So you can see I've got a blank instance, and it might be helpful just to leave this up side by side. Keep an eye on the clock. So I've got my blank instance of Directus. What I love about it is how easy it is to go in and build out our data model. So let's just start by creating a new collection. We've got Products and we're gonna give this a generated UUID for the primary key field. We'll add in a status, a date created, user updated, just some of the system fields, that are, like, built in utilities, basically. You know, we wanna keep track of what user updated, who, and when, just so we've got that information. Alright. So as far as products, we've, what, got a name for the product. We probably have a SKU number, SKU number, SKU part number. You know, you could have a couple of different items here. We've certainly got a description of the product, which we'll use our WYSIWYG editor inside Directus for. That way we can embed rich content. Sounds great. What else are we gonna have on the individual product? You know, a like, the price information may live on the the product variance there, so I'll keep it there. We're gonna have assets for this specific product as well. So I can go in and we'll create a new field and we'll use the relational files interface here which will actually create a junction table in our SQL database for us. So we'll do the product let's call this product Assets. Sounds great. And I could go into the advanced editor just to see what is happening behind the scenes. So we've got one collection called Products. We've got a related collection called Directus Files that is a built in system collection within Directus and then it automatically creates this junction table for us. Now if I wanted to adjust the name of the fields or control what this actual collection is named, I could turn off autofill and make that available. You know, and maybe I do want to add the concerning field or the reverse field to our products and we'll do a sort field just so I could control the primary images. Everything else looks great. We'll just save this, and now we've got some product data, right, or a product model where we can start uploading our data. Hey, this is a shirt and the SKU number is 1234. This is the best shirt ever. And we could even go in and upload an asset. We're probably gonna do a little bit of copyright infringement here. Just copy the image URL. We can upload this and I can do that by URL. Cannot fetch file from URL. Okay. Service unavailable. So I guess I'm going to have to download this image and then we'll just upload. So we get a pretty good idea of our basic functionality here. So now we've got our assets, and let's say I did want to browse this like a potential catalog. I can go into Directus, I could change this to the card view from our information sidebar and then I would just adjust my image source here. It looks like we don't support many to many fields there. So maybe we go back in and add a, like, a featured image. Featured image. Great. Alright. So this is just a single image that we want. So we'll go into that specific product, and I could pick this same image from the database. Alright. Great. So now we've got the featured image, we've got a name for the product, we've got our SKU number, and I can even go in and control how the image is cropped or not. So great. Now we've kind of got this, catalog looking module happening. Let's go in and finish fleshing out our data model. So I'll just clean this up a little bit. We've got our product variance and we'll use the generated UUID. Same thing, I will go in and add all these separate fields. Maybe we do have a sort field on the variant in case one variant takes priority over the other. Yeah. Kinda hit or miss. Alright. So we've got, our product variants. What we are going to do is create a many to one relationship back to our parent product. So this will be the product, and we're gonna choose the products collection. And this is a mini to one relationship. So I'm gonna go into the advanced field settings here. We'll just create a new, a one to many on the other side of the equation back on our products table for product variance. So now what I've done, in effect, is link these two collections or these two tables in our database together so that we can query and keep track of them. So now let's do what do we want to add for our other variant data, you know, so we've got color, that's just a string, we've got, size, etcetera. Great. We want to control a price here. You know, maybe we got that g ten, you know, unique identifier. What is that? The global trade identification number? Something like that. Let's go in and add a decimal. Actually, that'd be that'd be a float. And then we'll do maybe just 2 decimal places. Let's call this price. Great. Alright. So now we've got some items on our variants. Let's go back and actually look at how this works. So I go back into our product and we can see we've got our variants here and if I wanted to create a new variant, I can. Let's call this red, we'll call it small. The GTIN is 666 and the price for this is $25. We could go in and create another variant. This is red, this is medium, you know, 777, and the price is $16. Whatever. I can go in and actually set up our like what fields are displayed here. This doesn't look very great just to see the UUIDs. So let's sort that out, right. And we'll make this just a little bit bigger as well. Alright, so we go back into our data model, we go to our product variants, we look for our interface and we have this, control over do we want a list, do we want a table. Let's use the table in this case and we will add our color, size, maybe we'll do GTIN and price combinations. Now you also may have, like a variant that has a different image as well. So we could potentially add that as well. We'll just keep it simple for now and for the related fields, let's show the color. We'll add just like a little label for it so we know what it is, we'll add the size and we'll add size here. So let's see how that looks inside our PIM and great. Now we can see here's the different variants, there's the pricing, which, you know, maybe we wanna format that a little more, but great. And we can click on those to edit that information all that we need. Alright, now let's build out our categories, right? This is the only thing remaining on our data model. We want to build out categories for our different products. So we have a category, we'll use the generated UUID again, and not sure we need like a status on these, but maybe we do wanna sort and cool. Alright, so we've got a category name. Probably a short description for category. You know, we keep kinda splitting hairs which you on what detail you you need for something like this. If we look at, let's let's look at something like Pimcore. What what do they have available on their website? This looks really kinda messy to me. So we've got, different objects. We've got media. You know, I don't really know for certain how others set up their data model for this, but to me this is how I would do it. Now I could go in and have something like another classification for this. We could use, like, tags as a different type of taxonomy, a different way to organize the data. But I also have, you know, probably subcategories and parent categories. So Directus makes doing a recursive relationship like that very, very easy, where I can go in and do a many to 1 or a one to many relationship. So I'll just do one to many. We'll call this the, these are gonna be subcategories. Let's just do let's call let's do it the reverse. So this will be the parent category. We'll use the categories collection. So we're creating a relationship, a recursive relationship, and we're gonna do the subcategories. So we've got parent category which will be a single parent, then we have subcategories. Great. So what effect does that give us? Right? If I go into categories so let's create a new category we'll call it apparel. This is, let's call it actually, well, like outerwear or something like that. Great. Hoodies, jackets, and such. And we don't have a parent category or a subcategory yet, so let's just create a subcategory like hoodies. Hoodies. Great. We'll save that and now we can see we've got subcategories and, you know, potentially nest these. If I go back to our product, now we want to create a relationship with that category. So in this case, typically, a product may only have a single category. I I've seen some of these systems that will have, like, smart collections, which I think is a a really neat way to model it. And I think even Shopify, so I've got a a Shopify store set up here. Let's let's just take a look at how they handle it. If this can be in a multiple categories or no? So it looks just like a single category. You can set up different collections for it. So we'll keep this to be a mini to 1. So we've got a category and our related collection will be categories. And for our display template, maybe we just want to show the name of that category. Now, one of the other things I can do is control the interface to how the related values are displayed, just to make sure this looks how we want. So now if I go into our product, we can select the category hoodies or outerwear specifically. Great. Cool. Now, let's say what functionality, let's go back here. I always get ahead of myself. So now we've got a way to store and manage all of our product data. We've got a way to store all of our product assets, right? So I could go in and upload. Let's say I've got a PDF here. I could go in and upload this PDF. If I've got a spreadsheet, I could certainly do that as well. You know, I've got a CSV or whatever data that I've got, I can upload that and store it within this product. But, you know, what fun is it having these products in isolation? We wanna be able to use these specific products. So out of the box, because we've set up this data model, we know the structure and everything, Directus gives us the ability to well, it just gives us REST and GraphQL APIs. So I could go in and do something like this where we've got items. Products. I'm getting an error message because I haven't set up permissions, but let's imagine we wanted to allow anybody to access this product information. I can go to our setup, We'll go to access control and we'll go to our public role, which just controls what information is publicly available. And because none of this is secret at this point, let's go in and try it now. So if I hit refresh, we go to our products, we can see our product data. So there's the shirt, there's the SKU number, there's the description, we've got a featured image, UUID, And when it comes to our variants, we can't I can't see that information. That sucks. How do we fix this? I can actually do something like this, which is one of my favorite features inside Directus that I can query the related collections or the related fields from a single API call. So it's very GraphQL like even though this is REST based, and I, you know, there's a GraphQL API. I'm not a super huge fan. You may be. No sweat. So I could do something. We'll give, like, we'll use a wildcard here for all the root level fields. And then let's go in and do something like this where we have product variance and then we'll get all of the brute level fields for that. So as soon as I do that, now you can see I start to see that data that we want. And maybe I don't need all of this. Maybe we just need the ID product_variance dot color. I could get a little more sophisticated and copy paste dot size dot price dot g10. Alright. And now that will only return the data that I'm interested in which is nice prevents over fetching keeps things really fast when we are sending a lot of data over the network. Alright. So we've got the ability to store and manage all of our product assets. Let's control our product variations. How do we sync this data with an ecommerce system? Right? We're managing all of this in a single location. Maybe I've got Shopify, which is what we're gonna use. Maybe I've got an Amazon storefront setup. Maybe we've got 3 or 4 other things going on. Right? So how do we sync this information this data over? So I'm gonna go to our store and this is just the the Shopify demo store that I've set up. None of these products or anything that I created. Right? It looks like this is set up for snowboarding, which I tried once or twice, wasn't very good at it. But I'm gonna go into the settings of this and we're gonna try to find how to, get access to the API here. Let's look and see domains. Maybe it's here inside the apps. Okay. So we'll look for develop apps. We're gonna create an app that can connect with this storefront. Let's just take a look at it and see what it actually looks like on the front end. We got a password protection right now. We're gonna call this the Directus Shopify sync, maybe? We'll create this app inside the Shopify store. Alright. So it looks like the next thing that we'll need to do, we're probably gonna be using the admin API because we're not building a shopping experience. We're trying to send product data to Shopify, so that we can have that experience inside Shopify. So let's configure our access scopes. It's probably gonna be around products. And I'm not sure exactly which one of these I need. Product listings, product feeds, viewer manage products, let's go with that. We see the webhook subscriptions. We'll come back to webhooks. Let's just save that. What else do we need? We need some actual credentials. So we need an access token, a way to get that data. Use your client secret to verify incoming webhooks. Let's go ahead and install this app. This will give this app access to our storefront data. And we're only gonna reveal this token once and boom, we don't need it any longer. Not sure exactly how to set up the webhooks. Is that part of this? Start using the admin API. Do you have to create webhooks through the through the API? I don't know. If your app creates a webhook subscription oh, okay. So it looks like you do have to create webhooks using the API. Yeah. Alright. We'll come back to that. No worries. Alright. So now I've got this product information inside our account, I want to send it over to Shopify. How can I do that? Well, I could probably write some kind of custom app or I could just go in and use the flows, the the automation builder inside Directus. We're looking pretty good on time. We're at 36 minutes. So I'm just gonna make this full screen so we can dive in a little deeper. Let's call this send products to Shopify. Now how are we gonna trigger this? There's multiple ways to set up triggers. So I could manually trigger this. I could set it up on a cron job, so a regular interval. For now, I'm just gonna manually trigger this and we'll go we'll trigger this on our products section and maybe we'll require confirmation. Are you sure? Yes, we're sure. So the location here allows me to control where I can actually trigger this. So I'm just gonna trigger this on the collection page only. No. I could do both, you know, I could potentially send one product or maybe I want to send all of the products. We'll just keep that set up. Alright. So now let's go in. We'll save this flow and if we go back to our products page on the right hand side, I could see the option to send this to Shopify. So check an item. Are you sure? Yes, we'll run a flow. And if I go back to our flow, I refresh and I don't see my logs. Where are the logs? I ran this flow. Where is it? Let's make it not require a selection. Not sure if something happened here. What's what's going on? So we'll go to our flow. We'll hit send products to Shopify. Go to flows. Okay. Now they're showing up and we can see kind of the data that is being triggered here. So just collections, products. Great. Cool. So now, how do we get this actual data, right? We will go in and read the data. So let's just call it read products. That's gonna be the name of this operation. We're gonna select all the products from the collection products and then I could, like, filter this down if I wanted to. You know, maybe I I just leave it all for now. We're gonna select all the IDs and save it. And then once we read that data, we want to send to Shopify. Great. We'll, actually use the webhook HTTP request. How are we going to send the data? It's probably gonna be a post method. We're gonna go let's go back to Shopify really quickly. So we've got our storefront API documentation. Zoom in a little bit. Do we want the storefront? I don't think we do. So many tabs. So we go to admin. Oh, developer tools, API libraries. Oh, boy. Getting into the thick of it now. Let's go back to where's our actual store? Right. Explore, start using the admin API. Okay. Very confusing documentation here. Alright. So we'll go to our REST API reference. We'll look for products is probably what we want. We want to create a product. So there's what the product resource looks like. Here's our create a product, and there's our URL. So what does the base URL look like? Authentication. Okay. So it looks like the URL structure here is the Shopify unique shop ID or the shop slug, I'm assuming. My shopify.com/admin/api/ apiversion/resource. So our base URL setup so if we go back, will look something like this. Let's just pick up this actual URL. 100 apps, 100 hours, nice slugified version. Myshopifiedot com/ what was it? Real genius here. Okay. Let's go half screen. Myshopify.com/admin/api/ what? 2023 dash 10 dash products dot JSON. Okay. So there's our URL. Great. Now I've got that access token. Authentication. Looks like we have to pass that as a header as well. So we'll go into direct us. We'll add some headers. Got content dash type. That's gonna be application slash JSON. And then we've got x Shopify access token. And then we've got this special token that you're not gonna look at, you're not gonna read, you're not gonna try to use. Alright. So now we can send a request. So we go down. We're looking for products again. We're gonna create a product inside Shopify. Alright. So what are the details we need for our products? We got a product dot title, body HTML, that's our description. We've got a product vendor, that's one thing we forgot inside our pin that we could add. We've got the product status. So, let's go in and just do something really simple. We'll do products. Okay. So there's our product. Then we're gonna have a title for the product, what's that gonna be? Let's take a step back here. I'm just gonna save this. I need to see what actual data is coming from this call here before we get carried away. Create a new product. Can you create is there an endpoint for creating multiple products? That's what I'm interested in. Create a new product. Retrieve a list of products. Looks like we're just going to be able to create a single product at a time. So we may have to break this apart and do 2 separate flows. Imagine that I had another product. We'll call it test product. No. Let's not do that. Let's call it a hoodie. So it is hoodie 123. Let's go to unsplash maybe. Do a hoodie. And this looks like a hoodie with a statement so we'll just copy paste that. Okay. We'll import the file from the URL. Boom. We got a featured image. Put a description. Great. Alright. So now we get 2 products. We're just gonna select these products. We're gonna send the well, what happens if we don't select? We could still run this flow. We'll We'll take a look. We'll send these products down to our flow. And let's just look at the log. Right? So now I'm getting an array of products. Okay. And maybe we wanna do we're gonna break this apart because we're gonna have to send a single product to Shopify. But before we do that, I want to format products. Right? So we'll take our product. We'll run the format function. So Directus allows you to run arbitrary JavaScript inside the flows, which is really nice for stuff like this. We've got the data, so our products are gonna be constant products equals data dot read products. I think that's gonna be it. And then we have some formatting logic here. Right. Just thinking out loud. So now I'm just gonna save this really quick. And one of the other things that I like to do when I'm working with these flows is just copy the data I'm getting back right here, and I'm gonna put it inside my code editor. I've got a starter project here just in case we needed it. So now I've got the data structure. And if I look at Shopify gosh. We've got, like, 35 different tabs going on. That's the fun of doing these live is actually trying to figure out how to all how to keep it all sane on one screen. Admin, API. Alright. So we go to the admin API, Rest admin API. So we go to our products. What things do we need to pass to our products at a most at a minimum just to actually create these products. So here we go. Here's a new draft product type of setup. Creates a new draft product. And can we do let's just make this even more complicated. Right? We'll do like a split screen? Somebody still needs to teach me to properly arc. Alright. So I've got Directus, got our product. Okay. We got like 30 things going on here. A lot of fun. Alright. So, let's run a format script. We've got our data over here that is coming from our Directus database. Let's go in and add our logic here. We got products. Let's do Shopify products dot map. Oh, no. Shopify products equals products dot map product. Yeah. Products. And then we'll return something. We got our function there. And then at the end of this, we're gonna return Shopify products. So we're just gonna return the array of products we want from Shopify. Alright. So we got the products and then we're gonna return a title for the product which is gonna be the product dot what name. We need a comma there. Then we've got the body_html, that's gonna be product dot description. We've got product dot vendor. We don't have that product dot type. We could fetch that, and then for now, let's just try this and see how far that actually gets us. Product dot name. We're gonna return our Shopify products. Let's go back in and I'll just make this full screen. Do products. I will look for our sidebar. Where are you sidebar? There you are, mister sidebar. We'll send the products to Shopify. Let's just test our flow and see what came out the other side. Right. So this looks good. This is our format that we're gonna send to Shopify. I'm gonna take just my my headers that I've got here. Let's just stuff those in this, take my URL that we so carefully constructed. And, I'm just gonna leave that. We're gonna create a new flow. So we're gonna go in and create Shopify product and this will be triggered from that other flow, so triggered by another flow. We'll leave the response body blank and here we're just gonna do the HTTP request that we just did. So I'm gonna paste in that. I've got my headers. Still stuck on the clipboard somewhere. I'll edit raw value and now we've got that. And here, I think it is just going to be something like this where we do actually, hit save. We'll just disconnect this for now. Hit save. Go back to our other flow and trigger that flow, and we'll just see what trigger Shopify, create. And Directus will create a key for you, you can also rename this key, but this key, all the data that gets generated in this operation gets appended to the flow underneath this specific key. Let's just take a quick peek at time. How are we doing? We've got 20 minutes left. Great. And then we're just gonna trigger this flow. So we'll select the Shopify flow. It's okay to create these in parallel and then for our payload, we're gonna do simply this. We've got the double curly brackets. We've got the data. No. What's the name of that operation? Format product. Is that what the key is? We're just gonna use this key. So we're gonna trigger the flow and the data from that, format product, we're gonna pass that to that other flow. Alright. So fingers crossed, stay with me here, we are building cool stuff. So now we're gonna send these products to Shopify, running that flow. And we can see here we triggered this. We got back null null, this is what we sent to that. And if we go to our other flow, where we're actually creating, we can see that this ran twice, which is the what we actually want. We wanna see the the payload that we got was the product. So we're just gonna pick up the trigger. Payload and that's what we're gonna send. Great. Amazing. So, in our request body here, we're gonna do something like this where we've got, quotations, double curly bracket, trigger dot payload and trigger is, prefixed with a dollar sign just because it's a unique, special key to get the trigger data. And fingers crossed. Right? Can we send this data to Shopify? Can we build a working PIM in an hour? Let's test it out. We're gonna run I I don't think it matters if I select these or not. Let's go in. We'll see. We have our send products to Shopify. That ran. Hopefully triggered this other flow that we have. We got a Shopify product. Bad request. By accessing the Shopify, you agree to parameters are missing or invalid. Okay. So we're missing some parameters. Let's take a look at the actual data that we sent. Payload, got the product resource, body HTML, supports HTML formatting, product status dot draft, Shopify access token. It looks like the body is not defined. So, I think I goofed up and that and maybe we're just sending the trigger. Let's take a look. Did I get it wrong? Do we just need to send the trigger? So, let's make this more fun. Right. Let's go to our Shopify store. Alright. We're gonna close this guy. Alright. I'm on the products. I'm gonna hit refresh. Dun dun duh. I don't I don't see any products in here. Alright. So, let's go back. Obviously, I goofed somewhere again. And we're definitely in danger of running out of time here. We got a bad request, missing parameters. Okay. So there's the body, product title, product dot HTML. What are we missing? 100 apps, 100 hours. We've got our access token. We've got our content type. Required parameter. Did I spell something wrong? This is always the danger of working against the clock like this. What is this issue going to be? Request body is product. Alright. So product title body HTML. I don't see where this is going wrong. Shopify access token. Bad request. Let's take a look at our app. Do we have the wrong scopes or something? Let's just try one quick tweak. Go back to our send products for our format product. Let's just leave out the title. Right. So we'll just comment this out. Return Shopify products. Send products to Shopify, flows, bad request, product underscore. Is it the way that it is actually sending this data? Sometimes if we do something like this, where we got trigger. Boom boom, Ron. Created. Boom. Alright. So dramatic reveal time. Dun dun dun. We have sent this data to Shopify. Did we? Where do we go? Okay. So we've got a hoodie, we've got a shirt. There they are. Boom. Now they are in our system. That is how it's done, ladies and gentlemen. Where where we at on time? We got 13 minutes left. We have managed to sync product data. Let's let's just take this one step further if we can, and let's create a product with let's at least get the images over there. Right? So on our product resource, we've got a list of product images, create a new product image, create a product image using a source URL that will be downloaded to Shopify. Okay. So it looks like you can you may be able to pass these, and I'll just go in and very briefly let's just delete these products. Delete these products. Can't be undone. Those are deleted. Those are no longer in the system. Let's adjust our payload here. So we're gonna go back to the formatting option. We'll comment that back in. We can have that now and then on the Shopify side, we've got okay. So we got a product dot images. This will be images and then we've got an array And then what? Array of objects, and then we just got a source attribute. So the source if we look at our code again, Right. We've got our featured image. Let's just pluck that for now And, we're gonna do something like this where we've got the source is gonna be, HTTPS direct us app slash assets slash what was the name of that? Featured underscore image. Featured. Oop. Gotta do our little squirrellys featured image. Close that off. Should be good. The last piece of the puzzle for this is going to be enabling permissions for the actual files inside Directus as well, Because Shopify needs to access that. So 2 ways I could do it. I can add an access token, when I'm sending that call to Shopify Or in this case, I'm just gonna go in and we'll keep this really simple. Under direct as files, we'll do all access. And we're just going to send and cross our fingers hoping that this flow actually works. We could do the dramatic reveal inside Shopify. Right? I broke something trying to send the images over. Where did you go? Where did you go? Alright. What broke? Right? Clearly, we broke something. Great Shopify product. What do we send? Product title hoodie. Do we actually break something inside the I don't see my logs. 4 to 5 minutes ago, that's not correct. Run Shopify flows. Go into our flow. Less than a minute ago, featured image is not defined. Okay. So we did not have one with a featured image, and that broke. What did I do wrong? Again, always fun on the clock. I guess you have to actually use product dot featured image. Right? Featured image. That would make a ton of sense. Too much sense actually. Alright. Last time. Send products to Shopify. Did it work? Let's test our flow. Create Shopify product. Created. Good looking. Alright. So if we open up Shopify, we go to our products, we can see our hoodie. Here is our actual data, so there's the image that we've got, there's the shirt that we loaded up, and this is all coming from our pin, our back end, our single source of truth. So that is how it's done, ladies and gentlemen. We are syncing with Shopify. Last on our list, can we get this in 8 minutes? I have no idea. We've got a simple product catalog. Now, I don't think we could do this in 8 minutes, but let's give it a shot, right? We want to display all of our products on the front end of our website, so people can browse those. So because there are no rules, I am a huge fan of Tailwind UI and Tailwind CSS in general. They've got some great looking ecommerce components that we may just repurpose for this, like, here's a nice product list. And I've got a Nuxt starter application sitting here just ready to be used. So let's see if we can actually get this product information displayed and rendering on a product catalog in just a couple minutes. Alright, so we're gonna just forget Shopify for now. Let's simplify. Alright. This looks pretty good. This is a with border grid. We've got a view component here. We've got our products. Let's just copy this template. Alright, so we're gonna go to our index page. We probably got some script action that we'll do there. For now, I'm gonna if we just copy their products. Right. Constant products. Not gonna look great, and they've got an icon. So we'll do the star. Let's just omit the products ratings for now. Right? Just comment those up. Run this. If I look at local host, say now we've got got something cooking here. K. And now, let's actually fetch these products from Directus. We wanna do this live. Right? Going to do what, we got our products. Is it equal to await use directus? I've got a just a composable here to fetch this data using the rectus SDK. We're going to read items and that's gonna be products. And is this actually gonna be all we really need? Usually, when I am developing, I often tend to do stuff like this where I will just render out the data inside the browser just to make sure we're seeing we're getting the stuff that we need. Item/products failed. Oh, wait. Use direct us, read items, products. Why is this not working correctly? Well, that would be because I didn't actually set up the directus URL properly. We are at 5 minutes, so I'm gonna load my PIM URL here. That is my Directus base URL. The server token, probably not even needed in this setup, but, we'll just reload. Let's fire back up the dev server. Let's see if we can get this data rendering from Directus. Boom. So there's our actual data. Right? We no longer need this. Let's uncomment. And just looking at this data, we've got the v4productandproducts. We wanna render the product dot id, we'll have that information. The image source, because we can use Directus as an image provider it will transform assets for you. I'm just going to use the NUTS image tag which is already set up in this. We will use product dot featured image. We'll just leave alt text blank for now because there is there's none. We haven't got that set up. We got product dothref. We don't have one of those currently, but we could do slash products slash what? Product dot ID. That would be the probably the URL. We got a product dot name, we got a product dot price. What else? I wanna fix the template structure here and then maybe we want to render out, div. We'll do like a tailwindpros class and give this v html. We'll render the product description. Input must be a string received undefined. So if there let's make sure oh, forgot the item there. Boom. So there we have it. We have got our items rendered out. You know, we could continue fleshing this out with the help of Tailwind into a catalog. But now we have done several things, Right? Let's recap with our final two and a half minutes. We have created a PIM system that can store and manage all of our product data, store all of our product assets, control all of our product variations, we can sync that data with Shopify, and we can have a simple catalog. But, Bryant, you forgot one thing, our translations for this data. What if I want to have it in French or German or, Canadian? Bad bad joke for all all of our Canadian followers. Apologies there. So with a minute 30, how can we set up, translations for this? Right? How can I control other languages? So I'm gonna go into our products. The first thing I'm gonna do is just create a languages collection. We've got a language code. Alright. We've got a title or name of the language. Alright. And let's create 2 languages. We have EN, US, English. Are we gonna do it 55 seconds? Let's just say fr for fridge. I think that's the code. No idea. We'll go back to our product. Let's go in and there's a special translations interface within Directus. Clock is ticking, Brian. So we got the code, we got the direction field, that would probably be right to left. Use current language, we'll save this And then inside our product translations, we would go in and do, name, We would do a description. And where we at on time? 10 seconds. Brian, can you do it? 10, 9, 8, 7. How do we add translations for this? We can do English name. So close. We hit the 60 seconds. I'm just going to finish this or the 60 minutes. We can do the French translation here and save it. So, with Directus we can manage all of these different things. We hit time there, that felt really good though. We can manage translations or multilingual content. Alright. So that was a fun one. Multilingual content. Really enjoyed this one. Building a PIM in 60 minutes or less. There's obviously more to this, but my next steps would be to flesh out that sync engine a little bit, so that if we made updates inside Directus, I could send those over to Shopify. I could even use the Shopify webhooks in this case, if I made a change to that information in Shopify to sync it back into Directus. I would also probably go in and flesh out the translations interface just a little bit more. So that I have all the necessary fields. You know, and I may even have specific fields that we want to add to Shopify versus something like Amazon. So we could go in and do, you know, some more different some different channels that we wanna send this data to. But for 60 minutes, amazing to see what you can actually build with Directus. Stay tuned for the next episode. I'm sure we'll have a good one and you'll get to see me hack and slash and burn my way through another product.",[276],"f68324c1-69dc-4a09-82c9-136bbc1e12e7",[],{"id":172,"number":131,"show":122,"year":173,"episodes":279},[175,176,177,178,179,180,181,182,183,184,185],{"id":180,"slug":281,"vimeo_id":282,"description":283,"tile":284,"length":285,"resources":8,"people":286,"episode_number":288,"published":289,"title":290,"video_transcript_html":291,"video_transcript_text":292,"content":8,"seo":8,"status":130,"episode_people":293,"recommendations":295,"season":296},"ai-app","896095989","What if you could build an app that builds itself? That's the question Bryant seeks to answer in one hour on this AI themed episode. Follow along as he builds a custom Directus extension that connects with the OpenAI GPT-4 API and updates the projects underlying data model based on a simple prompt.","3ca1cb87-d47c-477a-af2f-a64dfe1bbd04",66,[287],{"name":199,"url":200},6,"2024-01-15","Mission: AI App Generator","\u003Cp>Speaker 0: Hi guys. Welcome back to another episode of 100 apps 100 hours. I'm your host Brian Gillespie, developer advocate at Directus, and today we are tackling the elephant in the room, AI. So we're gonna be building an AI app generator. By that, I mean an app that can update itself with new data models and new interesting ways to work.\u003C/p>\u003Cp>So what's the inspiration for this? So I've been, over the last couple weeks, I've been seeing all these videos on X or Twitter, whatever it is, that show, this application TL draw where you can go in and sketch something out inside this quick little application, click a button, and it will use the open AI vision API and some of their other tools, I guess, to actually generate real code that makes whatever you sketch come to life. So really excited by that. We're gonna try to replicate something similar here. I'm calling this the mighty morphing App Ranger because I grew up with Power Rangers.\u003C/p>\u003Cp>It was one of the I think it was one of my first Halloweens. So if you're new to the series, let's dive in. There are 2 rules. We have 60 minutes to plan and build this application, no more no less, and the second rule is just use whatever you have at your disposal. So in this AI episode it should be pretty interesting to see how far we can get with this.\u003C/p>\u003Cp>One of the ways that I wanna start here is just by charting things out, and we'll try to upload that chart to chat gpt or open AI and and spit something back out that we can use. So without further ado, let's dive in and start building. So like I said, I like to plan everything first. So let's just draw some stuff on our artboard here and kind of sketch out what we want this actual application to do. So the very beginning we're gonna draw a diagram of diagram diagram of an app.\u003C/p>\u003Cp>Upload, we want to, what, send that to the application. Let's shrink this down. That's really large. Alright, you can tell even though I use Figma quite a bit it's still not my strong suit. So we'll draw a diagram of an app.\u003C/p>\u003Cp>We will send, use OpenAI Vision to create a data model based on the diagram, and then we're going to update our app based on that code or data model automatically. So we want the app to automatically update itself, but before we dive into that, let's just try to proof the concept out. Right? So let's draw an app. Let's start with something simple like a CRM.\u003C/p>\u003Cp>Here's our CRM app. What are the different data models or the different tables, the the different collections as we call them inside Directus, which is what we're gonna be using for our back end and what we're gonna build our application with. So we've got contacts. Well, we probably got some organizations. We've got a relationship between those.\u003C/p>\u003Cp>We probably have some deals, and we can use some arrows for the relationships. And if we just look at contacts and orgs, we probably have maybe like a junction table or something here. I don't think it left enough room, but let's call this, what? Organization contacts. Give that a little more room.\u003C/p>\u003Cp>Okay. Alright. So there's our basic diagram. Maybe we go in and inspect some fields out for this as well. We got a first name, last name, email, phone, title.\u003C/p>\u003Cp>K. We've got a name for the organization. We've probably got an address. We've got, what, what else is on the address? I got lost on my Figma file here.\u003C/p>\u003Cp>So address, name, we got some contacts. We probably got organizations that those contacts belong to. That's probably good enough. For the deal, we probably have a deal name, deal value. And what else?\u003C/p>\u003Cp>Close date maybe? Alright. So that is a pretty simple CRM app. Let's make our diagram pretty. And now let's just take a screenshot of this and let's just proof this concept out.\u003C/p>\u003Cp>We can use just the regular chat GPT for this because it it has that underlying vision API that we're gonna use once we start building our app. But I can upload this into chat GPT and say something like this. You are a SQL, post grace, and direct us master. Please create a SQL query, SQL query that will create the data model for the this CRM application as designed in the in the what, in the diagram attached. Please fill in and make the data model more robust.\u003C/p>\u003Cp>So, honestly, I'm I'm just curious to see if this will actually work to begin with. What happens? Oh, no. Did I lose all of that? Did that really happen?\u003C/p>\u003Cp>Open this up. Oh, no. Okay. Well, that's a hazards of the job. Right?\u003C/p>\u003Cp>Restore recently closed. Please, you are a post grace and directus master. No hidden camera tricks on this show. Just fun here. Sometimes you fat finger one of these.\u003C/p>\u003Cp>Please create a SQL statement SQL query that will create the data model for our app as in the diagram. No. The conversation is not helpful so far. Let's do a new chat. Restore recently closed.\u003C/p>\u003Cp>Alright. Sometimes the technology doesn't work out so well. Please create a SQL query for Postgres, that's the database we're using, that will create an app create the data model for an app based on the diagram. Alright. I'm gonna copy this just so I don't have to what in the world is going on?\u003C/p>\u003Cp>Alright. Let's just try this again. Holy moly. Wow. Okay.\u003C/p>\u003Cp>So we've eaten up a lot of time there just messing with chat gpt. That's a lot of fun, but let's see what this comes up with. Looks like it has to create a SQL data model, SQL code. So if we just look at this, we've got a contacts table. That's good.\u003C/p>\u003Cp>We've got the organizations table. We've got a associative table. That's a junction table. We have a table for our deals, and then we have some kind of statement that is altering the table. Okay.\u003C/p>\u003Cp>Please rewrite to use UUID to add a primary key of ID for all the tables and use UIDs. Just want to make sure we all have a primary key of for each one. Alright. Cool. Okay.\u003C/p>\u003Cp>So now we've got the IDs. We're using the UUID instead of just an integer there. Alright. Great. Okay.\u003C/p>\u003Cp>So looking at this, we can test it out. Right? How are we gonna test this out? So, what I've got set up, for this app is basically a Docker Compose file that I'm using to generate my back end, which is Directus. And if I pull up my Directus instance and just log in, so we go to admin atexample.com.\u003C/p>\u003Cp>We do password. We'll log in and if I zoom in just a little bit you can see this is a completely blank app. Nothing here. So what Directus does behind the scenes, it sits alongside your Postgres database and it will introspect that database, which basically means anything that any changes that you make inside that database or any SQL database, doesn't have to just be Postgres, it will mirror those changes in real time. Sounds great.\u003C/p>\u003Cp>How does it work in application? Alright. So we go in, let's just pull up this database inside TablePlus. Right? So I could see all of my directus tables here.\u003C/p>\u003Cp>There's no other tables. We've got some items in here for, like, post GIS, just so we can do geo data inside here. But let's just copy and paste this inside the application, or inside table plus, and we're gonna run this SQL query. The only thing that I'm gonna do here is just change this application. I'm gonna change the organization contacts, And what I'm gonna do here, I'm just gonna change this so that there is a ID and that's the primary key instead of a composite key.\u003C/p>\u003Cp>But other than that, let's just run this thing. So hit run all. Okay. Altered table has been altered. If I refresh, we can see that we have created those different tables and, you know, the associated columns.\u003C/p>\u003Cp>Great. Now if I load up Directus, I can see that Directus, just refreshing the screen, has recognized that those tables are inside our database. So I can go through and click to configure these tables. We can see that my different fields are coming through, and if we click on those we can configure what the actual UI would look like for these things. So if I just go through and click on a few of them, maybe we want to hide this specific field.\u003C/p>\u003Cp>Directus really easily allows you to update this data and the presentation of it. So basically now we're just controlling the form that displays when we add a new contact. So let's just edit that a little bit, put the title down there. Great. Yeah.\u003C/p>\u003Cp>So if we go in, we refresh, we've got our contacts, and now I could go in and add my contact. Great. 555-555555. Looking great. Brian@directus dotio, and I'm a developer advocate.\u003C/p>\u003Cp>Cool. So now if I go into table plus here, we can also see there's my record. Right? So Directus is really nice in that it mirrors everything, but it also gives me a REST based API. So I could go in and any of those collections, if I were to, just give read access to just the general public, we could also create different user roles here.\u003C/p>\u003Cp>But if I were to go in and just copy my address I could do something like this where do items slash contacts and boom I get it ready to use REST or GraphQL API that we can use to build our back end with. That's great, this is really cool. How can we take this further, right, how can we make this application able to adjust itself, right, what does that actually look like? So Directus has built in automation, using flows. So flows you could set up, any type of simple or complex automation for your data.\u003C/p>\u003Cp>You can make third party API calls, receive incoming webhooks, all of those great things. And I'm thinking we can use a combination of flows and chat GPT or the OpenAI API, that's a mouthful, to actually be able to send this a prompt, have it update the data model automatically. Now there are multiple ways to adjust the schema or your data model of a Directus application. So if we look at the documentation, Directus actually has some schema endpoints where you can go and snapshot your data model. So you make a request, your schema Let's see if we see the sample response.\u003C/p>\u003Cp>This is the diff, so it actually takes a snapshot of the existing schema and then compares that, with what you've got and comes up with a difference. But it's basically a bunch of JSON data that represents the data model inside our database, inside our direct instance. It is a fairly specific and complex syntax, though. So I'm not sure about OpenAI being able to actually parse this. You know, I'm really concerned about the accuracy for this because it's kind of a a niche syntax that it has to adhere to.\u003C/p>\u003Cp>Right? So maybe we could go the SQL route because it seems like, OpenAI understands SQL pretty well. Just just guessing. I'm not sure if it does or not. I guess we'll find out.\u003C/p>\u003Cp>Right? So how can we actually do this? Direct is it doesn't allow you to run raw SQL, rightfully so. Easy way to blow up your database. But I think that's how we're gonna have to do this.\u003C/p>\u003Cp>So what can we do? Let's reach for a custom extension. So I'm just gonna go to the docs. We're going to create an extension, and so let's pull up our application. I'll just open this up.\u003C/p>\u003Cp>Alright, so we've got the npx command. Yes, We will create a Directus extension. And there are a couple different types of Directus extensions that you can create. I say a couple, I mean there's a lot of Directus Extensions, but what we're going to do is create an endpoint that we can call and run that actual with a SQL query provided by OpenAI and run that actual query. So first off foremost, Nat, maybe we can get, like, a big disclaimer on here.\u003C/p>\u003Cp>Do not do this sort of thing in production. Our engineering and security team will probably both be mad at me for this, but we're going to do the custom endpoint extension. What are we gonna call this? Let's just call it raw SQL is the name of our extension. We can use typescript, that's fine.\u003C/p>\u003Cp>And boom. So Directus will go through and scaffold out this extension. I could see it here inside my raw SQL. Cool. Alright.\u003C/p>\u003Cp>So now what I wanna do, I'm just gonna drag and drop this into our extensions endpoints folder directory and I'm gonna do one other thing here as well. When we actually build this extension, I'm just gonna drop it into the root of that raw SQL directory, instead of a distribution or dist directory, because it has to be inside the root directory for Directus to pick up on that. Alright. So now we're gonna go to extensions slash what endpoints/rawsequel. Okay.\u003C/p>\u003Cp>We're gonna do npm run dev, or it says npm run dev. So let's run that. And one of the other things that I've got, if you are working locally with Directus, you're building extensions, probably one of the things that you wanna do inside your config is set up this extensions auto reload, which basically means anytime that you rebuild that extension, which, you know, we're doing here, it will reload that without having to restart the direct assistance for you, which is nice when you are building. Alright. So if we take a look at our existing extension here, we hit save.\u003C/p>\u003Cp>It's gonna build that endpoint for us. And looks like Directus is reloading extensions. So now if I go to our Directus URL, local host 8055, and I do raw SQL, we get this hello world. Right? Great.\u003C/p>\u003Cp>But what do we want to actually do now? We want to actually run a a raw SQL query. So how do we actually make that work? How do we get that done? Underneath the hood, Directus is currently using a query builder called connects or nex or that.\u003C/p>\u003Cp>I never know how to say this thing, if you pronounce the k or not. But it does have a raw it has a raw query object or a raw method that you can call to actually run a raw statement against your database. Again, giant disclaimer, the lawyers say don't do this in production because it is super easy to blow up your database, and also don't trust AI fully to build your application for you. Lots of red flags on this one, but should be interesting nonetheless. Right?\u003C/p>\u003Cp>So what are we gonna do? We are going to look for our custom endpoints, and Directus gives us access to that underlying database instance. So the endpoint receives the router and the context. So if we do something like this, we've got our context, if I can actually type, and we're gonna do let's make this a post route and where we let's just call it raw SQL run. Alright.\u003C/p>\u003Cp>Cool. So we'll wrap this in a, like, a try catch. And what are we gonna do? We are going to pick up the query from the body. Yeah.\u003C/p>\u003Cp>Okay. And we are going to do what? We're going to do something like this where we have context dot databasecontext.database.raw, and then we're gonna run that query, and then we are going to return the result. Alright. Let's do our catch error, error dot message.\u003C/p>\u003Cp>Now let's see what happens if I refresh. Raw SQL, nothing happens on this, but let's go to run. Raw SQL does not exist. That's that's great. We want it to not work when we run that, but let's just open up something like Postman, so we could quickly test this.\u003C/p>\u003Cp>I I'm not really a curl guy. I always struggle with the command line. Alright. So we will pop this in. We've got raw SQL run, and we should be passing what?\u003C/p>\u003Cp>A raw JSON. K. And we've got our query. And let's just say select from what contacts. Alright.\u003C/p>\u003Cp>We hit posts. Raw SQL run does not exist. Why is that? Rawsequel/run. Why do we not exist?\u003C/p>\u003Cp>Great question. We are 36 minutes in. Raw SQL run does not exist. Alright. Let's see.\u003C/p>\u003Cp>We're getting a wait can't oh, the, this needs to be what? Async. Okay. Boom. Alright.\u003C/p>\u003Cp>So we just ran a raw database query. So you could see that query here, and it is returning the result from that query. Alright. Great. One of the other things that we could do here just to make this a little more robust would be to use the accountability object that gets included inside our services here.\u003C/p>\u003Cp>Where is this at? So each request we could take a look at the accountability. So we do something like this where only an admin user could do this. So if rec_.accountability ability dotuser.isisadmin? Is that what it is?\u003C/p>\u003Cp>I think that's it. Only admins can run raw SQL queries. Did I spell that right? Req.accountability. Let's see.\u003C/p>\u003Cp>Only admins can run raw SQL queries, so now we have to be logged in to actually run that. And, cool. So now if I go to our admin user and just generate an access token, and if I pass that in our authentication here as a bearer token. Okay. Cool.\u003C/p>\u003Cp>So we've added a little bit of auth to this endpoint to, you know, run our our SQL queries against the production database. Great. Alright. So now we've got our application here, and I refresh. I could see we've got contacts, deals, organizations.\u003C/p>\u003Cp>How we doing on time? Let's go in and let's add a new collection. We're just gonna call it, what, app, transformations, mighty morphin' power rangers. You know, we could probably even call it, like, AI migrations or something like that. Right?\u003C/p>\u003Cp>We'll generate a type for it. Do we really need these extra fields? Maybe we can do that. Alright. Alright.\u003C/p>\u003Cp>So let's give a let's have a here's the prompt. And I'm trying to think of ways that we could make this more robust. Let's see the SQL. Let's call this a query, and let's add another field. Maybe we could ask OpenAI to give us a way to undo.\u003C/p>\u003Cp>Okay. Alright. So now if we do this, Yeah. Maybe we should have had that status field as well. Has it been applied or not?\u003C/p>\u003Cp>So let's just do a drop down. So status, we'll do applied, unapplied, unapplied. Okay. And then any default values here will be unapplied. Cool.\u003C/p>\u003Cp>Alright. Great. Okay. So now we've got our AI migrations. You know, I I could go with the chart thing, but we could also make this fun where anybody can create new data for those or new applications.\u003C/p>\u003Cp>So let's do let's start by building our flow first. Alright. So flow flows are how we automate data inside Directus. So let's create a new flow, and we'll say call open AI, and we could trigger this a couple of different ways. We could trigger it manually or we could do it on a event hook in this case.\u003C/p>\u003Cp>So when a different event happens inside the platform, we trigger this flow. Let's do the action non blocking, and then we'll do anytime we create a new item in AI migrations, we'll trigger this flow. So I'm just gonna stop here. We'll go to AI migrations and we'll say create a postgrace data model for an LMS. Right?\u003C/p>\u003Cp>So I save this and if I go back to my flow and if I actually make this a little larger where you could see it, I can see my logs over here on the right hand side. Here's our payload. There's the prompt, and then we could see other things like our accountability. Is this a logged in user or not? Great.\u003C/p>\u003Cp>Alright. So let's move on to the next step. Right? Once we've got that prompt, we'd wanna pass that to OpenAI. So let's call, the the the what?\u003C/p>\u003Cp>OpenAI? I I don't even know what to call this. Call AI. We're gonna call the API, and we are looking for the webhook and request URL. So we'll go in and I'm pretty sure this will be a post request, but let's just open up OpenAI.\u003C/p>\u003Cp>Let's open up their platform. Got our API keys. So we're gonna need a new API key. This will be deleted by the time you guys watch, so please don't try to steal my API keys here. But if we look at, their documentation, right, we've got the different models like GPT 4.\u003C/p>\u003Cp>That's probably the the best one that we wanna use. GPT 4 turbo improved instruction following. So maybe that's the model we wanna use. Let's do this. Chat completions.\u003C/p>\u003Cp>So if we're doing this, we curl. Alright. Here's the endpoint that we're probably going to hit. Great. Okay.\u003C/p>\u003Cp>Alright. So if I just separate these 2, we'll drag that over. Get our text generation. So let's copy this URL. There's our chat completions.\u003C/p>\u003Cp>For our headers, we're gonna have content dash type. Got application JSON. Alright. And then we have our authorization. We have bear bear, and I got my API key.\u003C/p>\u003Cp>I'll just copy paste that there. Alright. So in the body of the request is where the meat and potatoes are here. Alright. So for our model, let's use the most expensive one.\u003C/p>\u003Cp>Right? Where's our models at? GPT, GPT 4 turbo. That one has a vision. Okay.\u003C/p>\u003Cp>Great. Let's just use this guy here, 1106. Fancy, fancy. Alright. So we've got our model that we're gonna use, and then we give it a prompt.\u003C/p>\u003Cp>Right? You are a helpful assistant, user assistant. Let me just edit this in my text editor real quick. Alright. So we're gonna do something like this where this is gonna be our trigger dot payload dot prompt.\u003C/p>\u003Cp>We'll delete the rest of these, and then let's give it some system instructions. Right? But let's use chat GPT for that as well. Right? So write some instructions for, chat, GPT 4 model, write some system instructions that will always make it generate SQL queries that, create well well rounded data models for apps as described by users.\u003C/p>\u003Cp>Let's see what it comes back with this. Really weird kind of having a conversation with AI in this case. Blah blah blah. Normalization. Include comments.\u003C/p>\u003Cp>Is this actually going to give me something or not? Let's take a look at where we're at. We got 25 minutes left. Chat GPT here is, like, killing me with the details here. Alright.\u003C/p>\u003Cp>So I guess we could just go with this. Let's just type something out. You are a Postgres and Directus expert full stack developer. Users will describe an application, and you will write a SQL query that will build the data model for that application. Use your expert knowledge to fill, to create a more robust application than the user has described has described.\u003C/p>\u003Cp>Do not use composite keys. Use only UID for primary keys. Call the primary key fields. What? ID.\u003C/p>\u003Cp>Use your best judgment to create the data model. Okay. Sounds good. There's our prompt. Okay.\u003C/p>\u003Cp>We'll just paste this into the body. Hit save. And let's just look at this real quick. There's our payload. Trigger payload prompt.\u003C/p>\u003Cp>Okay. Alright. Let's see what we're gonna get back from here, and then maybe I just want to go in and actually update that. So we'll say update migration. And maybe we can make this a 2 step process where, would maybe we'll just run it.\u003C/p>\u003Cp>We'll see. So we got update data. Let's go into our migration, and then we have trigger dot key. And the payload here is going to be query, and what did we call that? Call let's just do this.\u003C/p>\u003Cp>Just leave that blank for now. Let's give it a shot. Right? Let's see what happens. Please help me build a LMS.\u003C/p>\u003Cp>There are many courses. Each course has several modules and each module has many lessons. Okay. So if I save this, that should call the OpenAI API and return some data or return something. Right?\u003C/p>\u003Cp>Why are we not getting any logs here? Did this actually happen? What's going on here? Alright. Let's try it one more time.\u003C/p>\u003Cp>Or maybe it's still still building. What's going on? Not sure. We are at 21 minutes remaining. Okay.\u003C/p>\u003Cp>Yeah. It just takes a little while. Alright. So here's our payload from OpenAI. Okay.\u003C/p>\u003Cp>So we can already see there's a a problem here. Right? It is returning it's returning additional stuff, which kinda sucks. So we need to adjust our prompt for that. Right.\u003C/p>\u003Cp>You will only return the SQL query. And what was that I saw about JSON mode? Maybe that is something we can use here. Text generation, JSON mode. Okay.\u003C/p>\u003Cp>Yeah. That's probably what we need to enable. How do we do that? Response format type JSON objects. Okay.\u003C/p>\u003Cp>Let's give this a shot, shall we? Alright. So save this. And maybe I will, let me just delete these other prompts that we've got. And, actually, we could probably we've got enough data now that we should be able to pick this up, get choices back.\u003C/p>\u003Cp>Okay. So we would just have to access this, but let's run it one more time just to see. Let me build an LMS. There are many courses. Each course has several modules.\u003C/p>\u003Cp>Blah blah blah blah blah blah. So we are waiting for chat GPT to come back. But while we do that, oh, less than a minute. That's quick. Bad request.\u003C/p>\u003Cp>The dumb dumb. Okay. Response format, JSON, JSON object, messages. Goofed something up. We'll just leave that there.\u003C/p>\u003Cp>This is valid JSON. Looks like it. Let's try again. Alright. Delete.\u003C/p>\u003Cp>Okay. Prompt. Flows. Logs. Okay.\u003C/p>\u003Cp>So now it's doing its thing. What could we do next? Right? We're gonna run that SQL that it returns against that endpoint automatically. Kind of a scary thing, but let's see what we've got.\u003C/p>\u003Cp>Has it returned yet? Nope. Still hasn't returned. Alright. We'll wait on that.\u003C/p>\u003Cp>There we go. Okay. Alright. So was the payload coming back? Okay.\u003C/p>\u003Cp>So there's the SQL statement. Okay. And Alright. So it's just returning the query. When using JSON mode, produce some JSON via message for conversation.\u003C/p>\u003Cp>Response format. Yeah. I feel like we need to set this response format. I'm not sure what I did wrong the last time. Model response format type JSON object.\u003C/p>\u003Cp>This is invalid JSON or something? Not sure. It looks fine to me. Let's try it one more time. Otherwise, we'll just have to do something more interesting.\u003C/p>\u003Cp>I would just have to like replace those last 2 or 3 characters. Clipboard. Help me build an LMS. Cool. Alright.\u003C/p>\u003Cp>If this bricks, we'll just go back to what it was. Yep. Bad request. We cannot parse the JSON body of your request. Yeah.\u003C/p>\u003Cp>I don't I don't know why why it's doing that. Alright. Okay. So, anyway, there's our our data. Alright.\u003C/p>\u003Cp>So what are we getting back from the API when this actually works? We're getting something like this. So let's do a I'm just gonna copy and paste this. So here's our response. K.\u003C/p>\u003Cp>Great. Alright. So let's do an intermediate step where we're just gonna clean this data up. We'll go into run script, clean up query, and we're gonna do what? We're gonna get the data.\u003C/p>\u003Cp>So the query equals data dotco underscore call AI dot what? Dot data dot choices, first item in the array dot message dot content. Okay. And then we are going to, what, remove the first three characters. Right?\u003C/p>\u003Cp>Okay. And let's just use AI again. Right? Fun. Fun.\u003C/p>\u003Cp>Fun. Where did OpenAI go? Alright. Chat GPT. Let's create a new conversation.\u003C/p>\u003Cp>Maybe we could use 3.5 for this because it's faster. Write a JavaScript function to remove the 3 backticksandthe. SQL. Okay. Cool.\u003C/p>\u003Cp>So remove the back ticks in the SQL code. Cool. Alright. There we go. There's our cleaned SQL code function, and then we're just gonna return that.\u003C/p>\u003Cp>Return cleaned SQL code. Alright. So let's make sure that's gonna get what we need. We got data, We got replace content, choices message content. Okay.\u003C/p>\u003Cp>Let's give it a shot. Okay. And now for the next trick, we are going to post that, to our endpoint. Right? So we will grab our local host.\u003C/p>\u003Cp>We're gonna do raw SQL slash run. It's gonna be a post. We're gonna do authorization bearer and this is gonna be our token that we used here. Alright. So this is the token for our direct as user.\u003C/p>\u003Cp>What else do we need? Do we need anything else? Authorization. We probably ought to add content type. Application slash JSON.\u003C/p>\u003Cp>Great. Alright. Okay. So what are we looking at? What this should do is anytime we create a migration it will call OpenAI, ask it to generate that SQL, clean up that SQL, and then run that SQL command against our database.\u003C/p>\u003Cp>So if this goes right, it could basically be an application that could build itself. If it goes wrong, it's gonna blow this thing up entirely. Alright. So we got about 11 minutes left. Let's just test this out.\u003C/p>\u003Cp>Right? We've got our LMS prompt. Let's do it and see. Help me build an LMS. Each course has several modules.\u003C/p>\u003Cp>Each module has several lessons. Alright. Flows. Okay. So it hasn't come back yet.\u003C/p>\u003Cp>It is running. If I pop open Directus, let's open our Docker container. Go to the dashboard. We got Docker running here. We got our 100 apps.\u003C/p>\u003Cp>I can see the activity here, but we're not sure what's going on. Alright. So still running still running. We take a look at our data model. Nothing's happening yet.\u003C/p>\u003Cp>Here we go. Okay. So we got the data back from OpenAI. SQL code is not defined. Okay.\u003C/p>\u003Cp>So some issues, and it's still not returning what we need. The raw SQL query do not return anything else except for the raw SQL query. So sometimes AI is unreliable, and then we have what I must have goofed when I SQL code dot replace. Oh, duh. That's a query.\u003C/p>\u003Cp>Okay. Alright. Let's try again. Just delete this guy. Now this could get really fun where, like, you have AI itself, like on a cron job or something, just manually creating these prompts or automatically creating these prompts.\u003C/p>\u003Cp>So k. That kicks everything off. What are we gonna do in the meantime while we wait? Right? I don't know.\u003C/p>\u003Cp>I've got a a list of dad jokes that we could go through here if you guys want. I struggle with Roman numerals until I get to 159, then it just clicks. C l I x. Yeah. Okay.\u003C/p>\u003Cp>So if I look, here is my raw SQL that ran. If I look at the data model, no. Close. Alright. So let's look at our logs.\u003C/p>\u003Cp>Here's a minute ago. Internal server error cannot read properties of replace. Okay. So there's our raw SQL. Did I forget to actually call the SQL?\u003C/p>\u003Cp>What an idiot. Sometimes you have brain farts. Alright. So I forgot to actually paste in the the query there, so it wasn't running anything. So this is gonna be cleanup query.\u003C/p>\u003Cp>Alright. So if you remember, we are passing a query string into this. So we'll do this, clean up query, and that should solve the problem. Alright. Last time.\u003C/p>\u003Cp>Right? Hopefully this goes through. Help me build the LMS. Fun with AI and code. How we doing on time?\u003C/p>\u003Cp>Got 7 minutes left to prove this concept. All the engineers on our team are probably very upset if they're watching this. They put a lot of hard work into security and and making sure people don't do silly things like this. Alright. So webhook request, bad request, invalid payload.\u003C/p>\u003Cp>Why is it an invalid payload? JSON. Body query. Should we just wrap that in a what do we need to do here? Alright.\u003C/p>\u003Cp>So we reformat dot replace. Should we that bit? I don't know if we need to JSON stringify that. And I guess we could. Return JSON dot stringify.\u003C/p>\u003Cp>Not sure what that's gonna do. And then maybe even let's at least save that query so we can pull it up later. Right? So we'll update that. We'll do this with the, what, cleanup request.\u003C/p>\u003Cp>Cleanup query. Alright. Working on a time crunch here. What's going on? Fields to resolve.\u003C/p>\u003Cp>Save. Edit. Some kind of weird behavior happening. We save JSON Unexpected token. I don't know.\u003C/p>\u003Cp>Let's try it again. Help me build an LMS. Flows. Flows. Flows.\u003C/p>\u003Cp>Alright. So we'll just wait a minute. So also think about these AI. They take a little time, especially the the really fancy models. Right?\u003C/p>\u003Cp>Logs less than a minute ago. Bad request. Why can't we get this to run? Unexpected token. Payload.\u003C/p>\u003Cp>Did it update? Let's just look at the okay. So if we were to pop this in there, assuming we remove this, if we were to pop this inside table plus. Right? So if we open up our database, we go in, we run this SQL query.\u003C/p>\u003Cp>Does this actually work? Run all. Okay. So it doesn't really jive. Okay.\u003C/p>\u003Cp>So whatever we had previously should be what we go back to. Alright. When in doubt, turn to AI? I don't know, man. This one is challenging.\u003C/p>\u003Cp>I I thought we could totally get this done. We are not able to pass this. Let's just do this where we go into our query. We have return. Query.\u003C/p>\u003Cp>That's gonna be our cleaned SQL code. Alright. So we're returning that, and then we're gonna pass that cleanup query directly. So if I go into edit raw value, clean up query. Okay.\u003C/p>\u003Cp>And then I'm just gonna disconnect this piece for now. Man. Okay. Cutting it against the clock. I don't I don't know why I'm always up against the clock on this particular show.\u003C/p>\u003Cp>Alright. Let's try something else. Right? Help me build a CRM. No.\u003C/p>\u003Cp>Build an ecommerce platform for selling watches. Products, categories, prices. Let's just see what it comes back with. Flows less than a minute. Call OpenAI.\u003C/p>\u003Cp>Bad request. Okay. Why is that? Let's just try the standby here. Help me build.\u003C/p>\u003Cp>Alright. Okay. So that's doing its thing. We will try this as well, and otherwise, if it doesn't come back in the next 50 seconds, it feels like a public fail to me. Man, I was really hoping we could get this one to work properly.\u003C/p>\u003Cp>Sometimes it doesn't work out though. This one would be one to do a follow-up on though. Right? Oh, boy. Okay.\u003C/p>\u003Cp>Okay. Okay. Let's see what we got here. Right? Did this actually do what it wanted to do?\u003C/p>\u003Cp>Oh, there we go. How about that? Right? So now we've got our courses. Let's take a look.\u003C/p>\u003Cp>We've got a title and description. If we look at our lessons, what do we have? We've got title and content. We've got our modules that has our course relationship all ready to go for us. Right?\u003C/p>\u003Cp>Woah. Look at that, guys. At the buzzer, we came through. This is now a mighty morphing app Ranger that will build itself. Right?\u003C/p>\u003Cp>What can we do next with this? We could go in and create these prompts, but I could go in and do this, like, call a cron job. Let's just explore it further. I just wanted to take this, just to to one one more stop. So if we do create new prompts and at what, let's just make it whatever.\u003C/p>\u003Cp>I forget the syntax. What is the Google what's the Kron syntax? I wanna say it's like a we use the 6 digits syntax. Okay. Seconds, minutes, hours.\u003C/p>\u003Cp>Okay. So, again, chat gpt. Just for fun, what is create a cron syntax statement that will run every 2 minutes. Okay. Alright.\u003C/p>\u003Cp>So running every 2 minutes. There we go. Got that. So I'm just gonna go back into my Directus application. Where are you?\u003C/p>\u003Cp>Where did it go? Got too much going on. Is it over here? Okay. Alright.\u003C/p>\u003Cp>So we've got our cron syntax. That's gonna trigger and then but what if we call OpenAI again? Okay. I tell you what, let's do this on a follow-up. We're gonna eat up a lot of time here, but, just to prove that this is working again, let's do please help me build an ecommerce platform for selling watches.\u003C/p>\u003Cp>Pricing, let's just leave it at that. Right? Let's see what it does. Call create new prompts 3 minutes ago. Okay.\u003C/p>\u003Cp>This is coming back. And, again, I could watch my directus instance just to see if it is running that SQL statement. I don't see it running just yet. OpenAI is still thinking. But, wow, guys.\u003C/p>\u003Cp>This one was really fun. Pretty crazy what you can generate with AI. I hope you guys enjoyed this one. Stay tuned for more episodes in the series. Did it actually work?\u003C/p>\u003Cp>500 internal server error. What happened? Please make sure. Okay. It tried to kick something else out to to that.\u003C/p>\u003Cp>So it requires some fine tuning on the AI, but I think this is totally viable and totally an interesting project to build an app that can update and change itself. So that's it for this episode. Thanks for sticking around. Catch you on the next episode.\u003C/p>","Hi guys. Welcome back to another episode of 100 apps 100 hours. I'm your host Brian Gillespie, developer advocate at Directus, and today we are tackling the elephant in the room, AI. So we're gonna be building an AI app generator. By that, I mean an app that can update itself with new data models and new interesting ways to work. So what's the inspiration for this? So I've been, over the last couple weeks, I've been seeing all these videos on X or Twitter, whatever it is, that show, this application TL draw where you can go in and sketch something out inside this quick little application, click a button, and it will use the open AI vision API and some of their other tools, I guess, to actually generate real code that makes whatever you sketch come to life. So really excited by that. We're gonna try to replicate something similar here. I'm calling this the mighty morphing App Ranger because I grew up with Power Rangers. It was one of the I think it was one of my first Halloweens. So if you're new to the series, let's dive in. There are 2 rules. We have 60 minutes to plan and build this application, no more no less, and the second rule is just use whatever you have at your disposal. So in this AI episode it should be pretty interesting to see how far we can get with this. One of the ways that I wanna start here is just by charting things out, and we'll try to upload that chart to chat gpt or open AI and and spit something back out that we can use. So without further ado, let's dive in and start building. So like I said, I like to plan everything first. So let's just draw some stuff on our artboard here and kind of sketch out what we want this actual application to do. So the very beginning we're gonna draw a diagram of diagram diagram of an app. Upload, we want to, what, send that to the application. Let's shrink this down. That's really large. Alright, you can tell even though I use Figma quite a bit it's still not my strong suit. So we'll draw a diagram of an app. We will send, use OpenAI Vision to create a data model based on the diagram, and then we're going to update our app based on that code or data model automatically. So we want the app to automatically update itself, but before we dive into that, let's just try to proof the concept out. Right? So let's draw an app. Let's start with something simple like a CRM. Here's our CRM app. What are the different data models or the different tables, the the different collections as we call them inside Directus, which is what we're gonna be using for our back end and what we're gonna build our application with. So we've got contacts. Well, we probably got some organizations. We've got a relationship between those. We probably have some deals, and we can use some arrows for the relationships. And if we just look at contacts and orgs, we probably have maybe like a junction table or something here. I don't think it left enough room, but let's call this, what? Organization contacts. Give that a little more room. Okay. Alright. So there's our basic diagram. Maybe we go in and inspect some fields out for this as well. We got a first name, last name, email, phone, title. K. We've got a name for the organization. We've probably got an address. We've got, what, what else is on the address? I got lost on my Figma file here. So address, name, we got some contacts. We probably got organizations that those contacts belong to. That's probably good enough. For the deal, we probably have a deal name, deal value. And what else? Close date maybe? Alright. So that is a pretty simple CRM app. Let's make our diagram pretty. And now let's just take a screenshot of this and let's just proof this concept out. We can use just the regular chat GPT for this because it it has that underlying vision API that we're gonna use once we start building our app. But I can upload this into chat GPT and say something like this. You are a SQL, post grace, and direct us master. Please create a SQL query, SQL query that will create the data model for the this CRM application as designed in the in the what, in the diagram attached. Please fill in and make the data model more robust. So, honestly, I'm I'm just curious to see if this will actually work to begin with. What happens? Oh, no. Did I lose all of that? Did that really happen? Open this up. Oh, no. Okay. Well, that's a hazards of the job. Right? Restore recently closed. Please, you are a post grace and directus master. No hidden camera tricks on this show. Just fun here. Sometimes you fat finger one of these. Please create a SQL statement SQL query that will create the data model for our app as in the diagram. No. The conversation is not helpful so far. Let's do a new chat. Restore recently closed. Alright. Sometimes the technology doesn't work out so well. Please create a SQL query for Postgres, that's the database we're using, that will create an app create the data model for an app based on the diagram. Alright. I'm gonna copy this just so I don't have to what in the world is going on? Alright. Let's just try this again. Holy moly. Wow. Okay. So we've eaten up a lot of time there just messing with chat gpt. That's a lot of fun, but let's see what this comes up with. Looks like it has to create a SQL data model, SQL code. So if we just look at this, we've got a contacts table. That's good. We've got the organizations table. We've got a associative table. That's a junction table. We have a table for our deals, and then we have some kind of statement that is altering the table. Okay. Please rewrite to use UUID to add a primary key of ID for all the tables and use UIDs. Just want to make sure we all have a primary key of for each one. Alright. Cool. Okay. So now we've got the IDs. We're using the UUID instead of just an integer there. Alright. Great. Okay. So looking at this, we can test it out. Right? How are we gonna test this out? So, what I've got set up, for this app is basically a Docker Compose file that I'm using to generate my back end, which is Directus. And if I pull up my Directus instance and just log in, so we go to admin atexample.com. We do password. We'll log in and if I zoom in just a little bit you can see this is a completely blank app. Nothing here. So what Directus does behind the scenes, it sits alongside your Postgres database and it will introspect that database, which basically means anything that any changes that you make inside that database or any SQL database, doesn't have to just be Postgres, it will mirror those changes in real time. Sounds great. How does it work in application? Alright. So we go in, let's just pull up this database inside TablePlus. Right? So I could see all of my directus tables here. There's no other tables. We've got some items in here for, like, post GIS, just so we can do geo data inside here. But let's just copy and paste this inside the application, or inside table plus, and we're gonna run this SQL query. The only thing that I'm gonna do here is just change this application. I'm gonna change the organization contacts, And what I'm gonna do here, I'm just gonna change this so that there is a ID and that's the primary key instead of a composite key. But other than that, let's just run this thing. So hit run all. Okay. Altered table has been altered. If I refresh, we can see that we have created those different tables and, you know, the associated columns. Great. Now if I load up Directus, I can see that Directus, just refreshing the screen, has recognized that those tables are inside our database. So I can go through and click to configure these tables. We can see that my different fields are coming through, and if we click on those we can configure what the actual UI would look like for these things. So if I just go through and click on a few of them, maybe we want to hide this specific field. Directus really easily allows you to update this data and the presentation of it. So basically now we're just controlling the form that displays when we add a new contact. So let's just edit that a little bit, put the title down there. Great. Yeah. So if we go in, we refresh, we've got our contacts, and now I could go in and add my contact. Great. 555-555555. Looking great. Brian@directus dotio, and I'm a developer advocate. Cool. So now if I go into table plus here, we can also see there's my record. Right? So Directus is really nice in that it mirrors everything, but it also gives me a REST based API. So I could go in and any of those collections, if I were to, just give read access to just the general public, we could also create different user roles here. But if I were to go in and just copy my address I could do something like this where do items slash contacts and boom I get it ready to use REST or GraphQL API that we can use to build our back end with. That's great, this is really cool. How can we take this further, right, how can we make this application able to adjust itself, right, what does that actually look like? So Directus has built in automation, using flows. So flows you could set up, any type of simple or complex automation for your data. You can make third party API calls, receive incoming webhooks, all of those great things. And I'm thinking we can use a combination of flows and chat GPT or the OpenAI API, that's a mouthful, to actually be able to send this a prompt, have it update the data model automatically. Now there are multiple ways to adjust the schema or your data model of a Directus application. So if we look at the documentation, Directus actually has some schema endpoints where you can go and snapshot your data model. So you make a request, your schema Let's see if we see the sample response. This is the diff, so it actually takes a snapshot of the existing schema and then compares that, with what you've got and comes up with a difference. But it's basically a bunch of JSON data that represents the data model inside our database, inside our direct instance. It is a fairly specific and complex syntax, though. So I'm not sure about OpenAI being able to actually parse this. You know, I'm really concerned about the accuracy for this because it's kind of a a niche syntax that it has to adhere to. Right? So maybe we could go the SQL route because it seems like, OpenAI understands SQL pretty well. Just just guessing. I'm not sure if it does or not. I guess we'll find out. Right? So how can we actually do this? Direct is it doesn't allow you to run raw SQL, rightfully so. Easy way to blow up your database. But I think that's how we're gonna have to do this. So what can we do? Let's reach for a custom extension. So I'm just gonna go to the docs. We're going to create an extension, and so let's pull up our application. I'll just open this up. Alright, so we've got the npx command. Yes, We will create a Directus extension. And there are a couple different types of Directus extensions that you can create. I say a couple, I mean there's a lot of Directus Extensions, but what we're going to do is create an endpoint that we can call and run that actual with a SQL query provided by OpenAI and run that actual query. So first off foremost, Nat, maybe we can get, like, a big disclaimer on here. Do not do this sort of thing in production. Our engineering and security team will probably both be mad at me for this, but we're going to do the custom endpoint extension. What are we gonna call this? Let's just call it raw SQL is the name of our extension. We can use typescript, that's fine. And boom. So Directus will go through and scaffold out this extension. I could see it here inside my raw SQL. Cool. Alright. So now what I wanna do, I'm just gonna drag and drop this into our extensions endpoints folder directory and I'm gonna do one other thing here as well. When we actually build this extension, I'm just gonna drop it into the root of that raw SQL directory, instead of a distribution or dist directory, because it has to be inside the root directory for Directus to pick up on that. Alright. So now we're gonna go to extensions slash what endpoints/rawsequel. Okay. We're gonna do npm run dev, or it says npm run dev. So let's run that. And one of the other things that I've got, if you are working locally with Directus, you're building extensions, probably one of the things that you wanna do inside your config is set up this extensions auto reload, which basically means anytime that you rebuild that extension, which, you know, we're doing here, it will reload that without having to restart the direct assistance for you, which is nice when you are building. Alright. So if we take a look at our existing extension here, we hit save. It's gonna build that endpoint for us. And looks like Directus is reloading extensions. So now if I go to our Directus URL, local host 8055, and I do raw SQL, we get this hello world. Right? Great. But what do we want to actually do now? We want to actually run a a raw SQL query. So how do we actually make that work? How do we get that done? Underneath the hood, Directus is currently using a query builder called connects or nex or that. I never know how to say this thing, if you pronounce the k or not. But it does have a raw it has a raw query object or a raw method that you can call to actually run a raw statement against your database. Again, giant disclaimer, the lawyers say don't do this in production because it is super easy to blow up your database, and also don't trust AI fully to build your application for you. Lots of red flags on this one, but should be interesting nonetheless. Right? So what are we gonna do? We are going to look for our custom endpoints, and Directus gives us access to that underlying database instance. So the endpoint receives the router and the context. So if we do something like this, we've got our context, if I can actually type, and we're gonna do let's make this a post route and where we let's just call it raw SQL run. Alright. Cool. So we'll wrap this in a, like, a try catch. And what are we gonna do? We are going to pick up the query from the body. Yeah. Okay. And we are going to do what? We're going to do something like this where we have context dot databasecontext.database.raw, and then we're gonna run that query, and then we are going to return the result. Alright. Let's do our catch error, error dot message. Now let's see what happens if I refresh. Raw SQL, nothing happens on this, but let's go to run. Raw SQL does not exist. That's that's great. We want it to not work when we run that, but let's just open up something like Postman, so we could quickly test this. I I'm not really a curl guy. I always struggle with the command line. Alright. So we will pop this in. We've got raw SQL run, and we should be passing what? A raw JSON. K. And we've got our query. And let's just say select from what contacts. Alright. We hit posts. Raw SQL run does not exist. Why is that? Rawsequel/run. Why do we not exist? Great question. We are 36 minutes in. Raw SQL run does not exist. Alright. Let's see. We're getting a wait can't oh, the, this needs to be what? Async. Okay. Boom. Alright. So we just ran a raw database query. So you could see that query here, and it is returning the result from that query. Alright. Great. One of the other things that we could do here just to make this a little more robust would be to use the accountability object that gets included inside our services here. Where is this at? So each request we could take a look at the accountability. So we do something like this where only an admin user could do this. So if rec_.accountability ability dotuser.isisadmin? Is that what it is? I think that's it. Only admins can run raw SQL queries. Did I spell that right? Req.accountability. Let's see. Only admins can run raw SQL queries, so now we have to be logged in to actually run that. And, cool. So now if I go to our admin user and just generate an access token, and if I pass that in our authentication here as a bearer token. Okay. Cool. So we've added a little bit of auth to this endpoint to, you know, run our our SQL queries against the production database. Great. Alright. So now we've got our application here, and I refresh. I could see we've got contacts, deals, organizations. How we doing on time? Let's go in and let's add a new collection. We're just gonna call it, what, app, transformations, mighty morphin' power rangers. You know, we could probably even call it, like, AI migrations or something like that. Right? We'll generate a type for it. Do we really need these extra fields? Maybe we can do that. Alright. Alright. So let's give a let's have a here's the prompt. And I'm trying to think of ways that we could make this more robust. Let's see the SQL. Let's call this a query, and let's add another field. Maybe we could ask OpenAI to give us a way to undo. Okay. Alright. So now if we do this, Yeah. Maybe we should have had that status field as well. Has it been applied or not? So let's just do a drop down. So status, we'll do applied, unapplied, unapplied. Okay. And then any default values here will be unapplied. Cool. Alright. Great. Okay. So now we've got our AI migrations. You know, I I could go with the chart thing, but we could also make this fun where anybody can create new data for those or new applications. So let's do let's start by building our flow first. Alright. So flow flows are how we automate data inside Directus. So let's create a new flow, and we'll say call open AI, and we could trigger this a couple of different ways. We could trigger it manually or we could do it on a event hook in this case. So when a different event happens inside the platform, we trigger this flow. Let's do the action non blocking, and then we'll do anytime we create a new item in AI migrations, we'll trigger this flow. So I'm just gonna stop here. We'll go to AI migrations and we'll say create a postgrace data model for an LMS. Right? So I save this and if I go back to my flow and if I actually make this a little larger where you could see it, I can see my logs over here on the right hand side. Here's our payload. There's the prompt, and then we could see other things like our accountability. Is this a logged in user or not? Great. Alright. So let's move on to the next step. Right? Once we've got that prompt, we'd wanna pass that to OpenAI. So let's call, the the the what? OpenAI? I I don't even know what to call this. Call AI. We're gonna call the API, and we are looking for the webhook and request URL. So we'll go in and I'm pretty sure this will be a post request, but let's just open up OpenAI. Let's open up their platform. Got our API keys. So we're gonna need a new API key. This will be deleted by the time you guys watch, so please don't try to steal my API keys here. But if we look at, their documentation, right, we've got the different models like GPT 4. That's probably the the best one that we wanna use. GPT 4 turbo improved instruction following. So maybe that's the model we wanna use. Let's do this. Chat completions. So if we're doing this, we curl. Alright. Here's the endpoint that we're probably going to hit. Great. Okay. Alright. So if I just separate these 2, we'll drag that over. Get our text generation. So let's copy this URL. There's our chat completions. For our headers, we're gonna have content dash type. Got application JSON. Alright. And then we have our authorization. We have bear bear, and I got my API key. I'll just copy paste that there. Alright. So in the body of the request is where the meat and potatoes are here. Alright. So for our model, let's use the most expensive one. Right? Where's our models at? GPT, GPT 4 turbo. That one has a vision. Okay. Great. Let's just use this guy here, 1106. Fancy, fancy. Alright. So we've got our model that we're gonna use, and then we give it a prompt. Right? You are a helpful assistant, user assistant. Let me just edit this in my text editor real quick. Alright. So we're gonna do something like this where this is gonna be our trigger dot payload dot prompt. We'll delete the rest of these, and then let's give it some system instructions. Right? But let's use chat GPT for that as well. Right? So write some instructions for, chat, GPT 4 model, write some system instructions that will always make it generate SQL queries that, create well well rounded data models for apps as described by users. Let's see what it comes back with this. Really weird kind of having a conversation with AI in this case. Blah blah blah. Normalization. Include comments. Is this actually going to give me something or not? Let's take a look at where we're at. We got 25 minutes left. Chat GPT here is, like, killing me with the details here. Alright. So I guess we could just go with this. Let's just type something out. You are a Postgres and Directus expert full stack developer. Users will describe an application, and you will write a SQL query that will build the data model for that application. Use your expert knowledge to fill, to create a more robust application than the user has described has described. Do not use composite keys. Use only UID for primary keys. Call the primary key fields. What? ID. Use your best judgment to create the data model. Okay. Sounds good. There's our prompt. Okay. We'll just paste this into the body. Hit save. And let's just look at this real quick. There's our payload. Trigger payload prompt. Okay. Alright. Let's see what we're gonna get back from here, and then maybe I just want to go in and actually update that. So we'll say update migration. And maybe we can make this a 2 step process where, would maybe we'll just run it. We'll see. So we got update data. Let's go into our migration, and then we have trigger dot key. And the payload here is going to be query, and what did we call that? Call let's just do this. Just leave that blank for now. Let's give it a shot. Right? Let's see what happens. Please help me build a LMS. There are many courses. Each course has several modules and each module has many lessons. Okay. So if I save this, that should call the OpenAI API and return some data or return something. Right? Why are we not getting any logs here? Did this actually happen? What's going on here? Alright. Let's try it one more time. Or maybe it's still still building. What's going on? Not sure. We are at 21 minutes remaining. Okay. Yeah. It just takes a little while. Alright. So here's our payload from OpenAI. Okay. So we can already see there's a a problem here. Right? It is returning it's returning additional stuff, which kinda sucks. So we need to adjust our prompt for that. Right. You will only return the SQL query. And what was that I saw about JSON mode? Maybe that is something we can use here. Text generation, JSON mode. Okay. Yeah. That's probably what we need to enable. How do we do that? Response format type JSON objects. Okay. Let's give this a shot, shall we? Alright. So save this. And maybe I will, let me just delete these other prompts that we've got. And, actually, we could probably we've got enough data now that we should be able to pick this up, get choices back. Okay. So we would just have to access this, but let's run it one more time just to see. Let me build an LMS. There are many courses. Each course has several modules. Blah blah blah blah blah blah. So we are waiting for chat GPT to come back. But while we do that, oh, less than a minute. That's quick. Bad request. The dumb dumb. Okay. Response format, JSON, JSON object, messages. Goofed something up. We'll just leave that there. This is valid JSON. Looks like it. Let's try again. Alright. Delete. Okay. Prompt. Flows. Logs. Okay. So now it's doing its thing. What could we do next? Right? We're gonna run that SQL that it returns against that endpoint automatically. Kind of a scary thing, but let's see what we've got. Has it returned yet? Nope. Still hasn't returned. Alright. We'll wait on that. There we go. Okay. Alright. So was the payload coming back? Okay. So there's the SQL statement. Okay. And Alright. So it's just returning the query. When using JSON mode, produce some JSON via message for conversation. Response format. Yeah. I feel like we need to set this response format. I'm not sure what I did wrong the last time. Model response format type JSON object. This is invalid JSON or something? Not sure. It looks fine to me. Let's try it one more time. Otherwise, we'll just have to do something more interesting. I would just have to like replace those last 2 or 3 characters. Clipboard. Help me build an LMS. Cool. Alright. If this bricks, we'll just go back to what it was. Yep. Bad request. We cannot parse the JSON body of your request. Yeah. I don't I don't know why why it's doing that. Alright. Okay. So, anyway, there's our our data. Alright. So what are we getting back from the API when this actually works? We're getting something like this. So let's do a I'm just gonna copy and paste this. So here's our response. K. Great. Alright. So let's do an intermediate step where we're just gonna clean this data up. We'll go into run script, clean up query, and we're gonna do what? We're gonna get the data. So the query equals data dotco underscore call AI dot what? Dot data dot choices, first item in the array dot message dot content. Okay. And then we are going to, what, remove the first three characters. Right? Okay. And let's just use AI again. Right? Fun. Fun. Fun. Where did OpenAI go? Alright. Chat GPT. Let's create a new conversation. Maybe we could use 3.5 for this because it's faster. Write a JavaScript function to remove the 3 backticksandthe. SQL. Okay. Cool. So remove the back ticks in the SQL code. Cool. Alright. There we go. There's our cleaned SQL code function, and then we're just gonna return that. Return cleaned SQL code. Alright. So let's make sure that's gonna get what we need. We got data, We got replace content, choices message content. Okay. Let's give it a shot. Okay. And now for the next trick, we are going to post that, to our endpoint. Right? So we will grab our local host. We're gonna do raw SQL slash run. It's gonna be a post. We're gonna do authorization bearer and this is gonna be our token that we used here. Alright. So this is the token for our direct as user. What else do we need? Do we need anything else? Authorization. We probably ought to add content type. Application slash JSON. Great. Alright. Okay. So what are we looking at? What this should do is anytime we create a migration it will call OpenAI, ask it to generate that SQL, clean up that SQL, and then run that SQL command against our database. So if this goes right, it could basically be an application that could build itself. If it goes wrong, it's gonna blow this thing up entirely. Alright. So we got about 11 minutes left. Let's just test this out. Right? We've got our LMS prompt. Let's do it and see. Help me build an LMS. Each course has several modules. Each module has several lessons. Alright. Flows. Okay. So it hasn't come back yet. It is running. If I pop open Directus, let's open our Docker container. Go to the dashboard. We got Docker running here. We got our 100 apps. I can see the activity here, but we're not sure what's going on. Alright. So still running still running. We take a look at our data model. Nothing's happening yet. Here we go. Okay. So we got the data back from OpenAI. SQL code is not defined. Okay. So some issues, and it's still not returning what we need. The raw SQL query do not return anything else except for the raw SQL query. So sometimes AI is unreliable, and then we have what I must have goofed when I SQL code dot replace. Oh, duh. That's a query. Okay. Alright. Let's try again. Just delete this guy. Now this could get really fun where, like, you have AI itself, like on a cron job or something, just manually creating these prompts or automatically creating these prompts. So k. That kicks everything off. What are we gonna do in the meantime while we wait? Right? I don't know. I've got a a list of dad jokes that we could go through here if you guys want. I struggle with Roman numerals until I get to 159, then it just clicks. C l I x. Yeah. Okay. So if I look, here is my raw SQL that ran. If I look at the data model, no. Close. Alright. So let's look at our logs. Here's a minute ago. Internal server error cannot read properties of replace. Okay. So there's our raw SQL. Did I forget to actually call the SQL? What an idiot. Sometimes you have brain farts. Alright. So I forgot to actually paste in the the query there, so it wasn't running anything. So this is gonna be cleanup query. Alright. So if you remember, we are passing a query string into this. So we'll do this, clean up query, and that should solve the problem. Alright. Last time. Right? Hopefully this goes through. Help me build the LMS. Fun with AI and code. How we doing on time? Got 7 minutes left to prove this concept. All the engineers on our team are probably very upset if they're watching this. They put a lot of hard work into security and and making sure people don't do silly things like this. Alright. So webhook request, bad request, invalid payload. Why is it an invalid payload? JSON. Body query. Should we just wrap that in a what do we need to do here? Alright. So we reformat dot replace. Should we that bit? I don't know if we need to JSON stringify that. And I guess we could. Return JSON dot stringify. Not sure what that's gonna do. And then maybe even let's at least save that query so we can pull it up later. Right? So we'll update that. We'll do this with the, what, cleanup request. Cleanup query. Alright. Working on a time crunch here. What's going on? Fields to resolve. Save. Edit. Some kind of weird behavior happening. We save JSON Unexpected token. I don't know. Let's try it again. Help me build an LMS. Flows. Flows. Flows. Alright. So we'll just wait a minute. So also think about these AI. They take a little time, especially the the really fancy models. Right? Logs less than a minute ago. Bad request. Why can't we get this to run? Unexpected token. Payload. Did it update? Let's just look at the okay. So if we were to pop this in there, assuming we remove this, if we were to pop this inside table plus. Right? So if we open up our database, we go in, we run this SQL query. Does this actually work? Run all. Okay. So it doesn't really jive. Okay. So whatever we had previously should be what we go back to. Alright. When in doubt, turn to AI? I don't know, man. This one is challenging. I I thought we could totally get this done. We are not able to pass this. Let's just do this where we go into our query. We have return. Query. That's gonna be our cleaned SQL code. Alright. So we're returning that, and then we're gonna pass that cleanup query directly. So if I go into edit raw value, clean up query. Okay. And then I'm just gonna disconnect this piece for now. Man. Okay. Cutting it against the clock. I don't I don't know why I'm always up against the clock on this particular show. Alright. Let's try something else. Right? Help me build a CRM. No. Build an ecommerce platform for selling watches. Products, categories, prices. Let's just see what it comes back with. Flows less than a minute. Call OpenAI. Bad request. Okay. Why is that? Let's just try the standby here. Help me build. Alright. Okay. So that's doing its thing. We will try this as well, and otherwise, if it doesn't come back in the next 50 seconds, it feels like a public fail to me. Man, I was really hoping we could get this one to work properly. Sometimes it doesn't work out though. This one would be one to do a follow-up on though. Right? Oh, boy. Okay. Okay. Okay. Let's see what we got here. Right? Did this actually do what it wanted to do? Oh, there we go. How about that? Right? So now we've got our courses. Let's take a look. We've got a title and description. If we look at our lessons, what do we have? We've got title and content. We've got our modules that has our course relationship all ready to go for us. Right? Woah. Look at that, guys. At the buzzer, we came through. This is now a mighty morphing app Ranger that will build itself. Right? What can we do next with this? We could go in and create these prompts, but I could go in and do this, like, call a cron job. Let's just explore it further. I just wanted to take this, just to to one one more stop. So if we do create new prompts and at what, let's just make it whatever. I forget the syntax. What is the Google what's the Kron syntax? I wanna say it's like a we use the 6 digits syntax. Okay. Seconds, minutes, hours. Okay. So, again, chat gpt. Just for fun, what is create a cron syntax statement that will run every 2 minutes. Okay. Alright. So running every 2 minutes. There we go. Got that. So I'm just gonna go back into my Directus application. Where are you? Where did it go? Got too much going on. Is it over here? Okay. Alright. So we've got our cron syntax. That's gonna trigger and then but what if we call OpenAI again? Okay. I tell you what, let's do this on a follow-up. We're gonna eat up a lot of time here, but, just to prove that this is working again, let's do please help me build an ecommerce platform for selling watches. Pricing, let's just leave it at that. Right? Let's see what it does. Call create new prompts 3 minutes ago. Okay. This is coming back. And, again, I could watch my directus instance just to see if it is running that SQL statement. I don't see it running just yet. OpenAI is still thinking. But, wow, guys. This one was really fun. Pretty crazy what you can generate with AI. I hope you guys enjoyed this one. Stay tuned for more episodes in the series. Did it actually work? 500 internal server error. What happened? Please make sure. Okay. It tried to kick something else out to to that. So it requires some fine tuning on the AI, but I think this is totally viable and totally an interesting project to build an app that can update and change itself. So that's it for this episode. Thanks for sticking around. Catch you on the next episode.",[294],"3f644a06-47d4-4ab7-b13c-20825b1df3b3",[],{"id":172,"number":131,"show":122,"year":173,"episodes":297},[175,176,177,178,179,180,181,182,183,184,185],{"id":181,"slug":299,"vimeo_id":300,"description":301,"tile":302,"length":285,"resources":8,"people":303,"episode_number":305,"published":306,"title":307,"video_transcript_html":308,"video_transcript_text":309,"content":8,"seo":8,"status":130,"episode_people":310,"recommendations":312,"season":313},"community-platform","896091486","Every thriving community needs a home. Dive right in alongside Bryant and he builds a community platform inspired by Circle. He races against the clock to build an engaging and interactive community space in just one hour.","903b4fe1-bb22-4e2f-b946-7fce5cff7a11",[304],{"name":199,"url":200},7,"2024-01-22","Mission: Community Platform","\u003Cp>Speaker 0: Hi guys. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus. And today, we will be building a community platform. Now, this is one that is near and dear to my heart because I have built a community for sign and print shop owners myself.\u003C/p>\u003Cp>We use Facebook Groups for that. Not the best community platform, but it is free and everybody already has a Facebook account. But in just a quick look at the research for this episode, There's quite a few different tools out there that I located, Circle being one of the more popular ones. It's not exactly a forum, it's not exactly a course, it's not exactly chat, it's all of those things. So the pieces that we're really going to dive into today are this concept that they have of spaces.\u003C/p>\u003Cp>So each one of these items in the left navigation is a space. A space can be, either private or public. So I can see here some of the circle experts and this is their their own example community. Right? They do have courses in here.\u003C/p>\u003Cp>We've already done an episode on LMS, so we won't really touch on that. But, if we get into this, we could see what's new in the product. So this is an update we're gonna post. We've got some comments down at the bottom, I'm assuming, and you had to be logged in to comment. So how do we kinda recreate some of this functionality?\u003C/p>\u003Cp>It looks like they have weekly events here, but we're probably not seeing those. And maybe here's something similar. So anyway, that's what we're building. If you, have watched any of the other episodes, you probably already know the rules. We've got 60 minutes to plan and build, no more, no less.\u003C/p>\u003Cp>And the second rule, which is not really a rule, is use whatever you have at your disposal. So, any of the tools that are out there, AI, Tailwind CSS is one of my favorite ones. We're going to be building this platform on Directus as well. That'll be our back end. You could probably even actually build the entire thing inside Directus, but we'll probably go for a custom UI on the front end.\u003C/p>\u003Cp>Alright. So, sounds great. Let's rock and roll. So we'll start the 60 minute timer and away we go. So anytime I build an app, I always love to just kinda flesh out the functionality ahead of time.\u003C/p>\u003Cp>So if we're looking at this specific app, maybe I'll just pull up a text box inside Figma here. Here's the functionality that we really want out of this app. Right? We want to create and manage different spaces. Spaces for a how do I can I constrain this?\u003C/p>\u003Cp>Yeah. There we go. For a community. Maybe we have events as well that we're going to list. List and create and manage events.\u003C/p>\u003Cp>Events. We need to be able to log in. Users can log in. Create posts. Right?\u003C/p>\u003Cp>Is that what we're going to call that? Maybe a thread? We'll get into it. Right? We'll create some posts and add comments to those posts.\u003C/p>\u003Cp>That feels like a a pretty good chunk of functionality. I don't know if we'll get to all that or not. That's, like, actually building the UI for that, could be fun. Right? Alright.\u003C/p>\u003Cp>So let's just, like, flesh out our our actual data model here as well. So again, I'm not the savviest Figma user, but I try. I'm a tried. Alright. So what do we have?\u003C/p>\u003Cp>We've got spaces. So those are gonna be our little homes for all this different stuff, this different type of content. Right? Then we've probably got, this is where we get into the naming. Right?\u003C/p>\u003Cp>Is it a a thread? Is this a what what are these things called? Are these posts? Yeah. Let's go with that.\u003C/p>\u003Cp>Right? This is a post. And a post has some comments. Probably has a probably has, like, a post type, I'm assuming. K.\u003C/p>\u003Cp>Let's look at what else we've got here. Share your wins. Log in. Yeah. Okay.\u003C/p>\u003Cp>So these are different posts. You can upload images to a post. You can add comments. You can like. So we probably got some likes in there.\u003C/p>\u003Cp>And I don't know that we'll get to that either. But, what else do we have? We have users. That's gonna be taken care of by Directus because it gives us that out of the box, which is nice. That'll be our Directus users collection.\u003C/p>\u003Cp>And what else do we need for a function community? Right. Are events gonna be a separate thing than posts? I don't know. Let's we'll try it out.\u003C/p>\u003Cp>Cool. Let's just add that to the end here in case we we don't have time to get to it. Alright. So this feels pretty good. We're closing in on 4 minutes or so in the planning stage.\u003C/p>\u003Cp>Alright. So what do we have set up? We have, we got Directus going here. I've got a Directus instance that is totally blank, so I just reload. You'll see there are 0 collections.\u003C/p>\u003Cp>And on the front end, I've got a starter Nuxt application that just basically has the Directus module or the Directus plug in already preconfigured. We're using the SDK to communicate with the Directus instance, but I've just got Directus spun up on a local Docker instance. So, really fancy app here. We've got Tailwind. We're using Nuxt UI as well on that front end.\u003C/p>\u003Cp>So got quite a few things going on there. Alright. So first and foremost, we've got spaces. Let's go ahead and create our data model. Directus makes this super easy.\u003C/p>\u003Cp>We'll just go into our blank instance. We'll create our first collection. We're going to call it spaces. So those are the different spaces here. We'll generate the UID.\u003C/p>\u003Cp>We'll probably have a a status for the space. What's the sort order for the spaces? Who created these? When were they created? That's all pretty standard.\u003C/p>\u003Cp>Let's just take a look at the network requests on this to see if we can actually pick up some of their data model. Alright. Space groups. Alright. There's the circle community.\u003C/p>\u003Cp>Allow members to spaces. What else do we have? There's a slug for the space. So that's, that's a handy piece of information to have. Spaces, discussion.\u003C/p>\u003Cp>Okay. So we've got a cover image. Cover image URL. And this is via their API, so it doesn't necessarily represent the underlying database structure, but this data is coming from somewhere. So we're gonna have a name for the space.\u003C/p>\u003Cp>Name for the space. That's great. Alright. Do they have a description? Space description.\u003C/p>\u003Cp>Space name. We're definitely gonna have a slug, so that's great. We'll roll with that. Advanced field creation mode and, when I need something like a slug or a value that URL safe, if I go into advanced mode, I have Slugify available. So that will automatically transform the input as I type.\u003C/p>\u003Cp>Lock screen heading. I don't know, man. Let's just give it a description. And let's keep that non WYSIWYG. And last but not least, for our space, spaces, Let's add a cover image.\u003C/p>\u003Cp>So we've got our cover image. Right? And let's see if they had do they have a type of space? Space, slug. Visibility, post type is basic.\u003C/p>\u003Cp>I I wanna say there's probably some setting in here to control what these things look like, whether this is like a discussion or this is an event or a course. As we're fetching these, okay, that's a course. Internal spaces API. Maybe a type. Is there a type?\u003C/p>\u003Cp>Course type. Type is a banner. K. Those are yeah. No.\u003C/p>\u003Cp>I I don't wanna waste time on it. Right? Cool. So status, we'll just leave blank for now. Is this published or is it available?\u003C/p>\u003Cp>And then let's say let's add 1, like, is private. This will be a toggle. So the Boolean values Boolean value, I always used to struggle with that. That's what we'll save in the database. Is this space private or not?\u003C/p>\u003Cp>And then we can do a little cleanup here on our fields. So name, slug, description, maybe we pop that right beside there. And status wise, let's just roll with it. Right? So we go in, we create our first space.\u003C/p>\u003Cp>Maybe we wanna call this discussion, and that'll be at the slug for that will be discussion. Here's where we discuss stuff. Great. Is it private? Yes.\u003C/p>\u003Cp>Let's make it private. And then we will let's upload a cover image. Right? Unsplash. Let's just search for discussion.\u003C/p>\u003Cp>Okay. Images that are not seem to be working. Okay. Maybe some type of Internet issue. We're working here locally, anyway.\u003C/p>\u003Cp>Let's just grab from Circle. Right? We're already working with this specific thing. Do they have discussion? Where's the best practices for engagement?\u003C/p>\u003Cp>Yeah. Fine. Let's roll with it. Cool. Alright.\u003C/p>\u003Cp>So now we've got our first space. What else do we need to add to this? Right? So we've got posts for the spaces. Then each one of those posts could have comments, potentially.\u003C/p>\u003Cp>Right? So, let's just go in and create another. We'll just call it announcements as the next space. Announcements, read only channel for important announcements. Very fancy.\u003C/p>\u003Cp>Important announcements. Alright. So that is not private. Let's make both of these published just to to have it. Great.\u003C/p>\u003Cp>Okay, so we'll go back into our settings and let's add the next item. Let's add posts. We use the UUID. Do we have a status for the post? Yeah.\u003C/p>\u003Cp>Probably. Date created, date updated, is there a sort for the post? Do we wanna control the sort order on those? Yeah. Maybe potentially you do.\u003C/p>\u003Cp>Alright. So if we take a look at circle here. Let's dissect one of these posts. Right? So a post has a title, looks like.\u003C/p>\u003Cp>Also has a user that created the post. So Directus has taken care of that for us out of the box. We probably got a date that this was created. We've got some rich text for the post. So that'll be the post content, maybe?\u003C/p>\u003Cp>Post body? You do that. Let's just keep it as content. It seems pretty straightforward. Naming stuff is always the the hardest part about development.\u003C/p>\u003Cp>What else do we have? We got that cover image for the post. Right? Cover image for our post so that, you know, if we want to upload a cover image, we can. Sweet.\u003C/p>\u003Cp>Looks great. Let's go in and just create one of those posts. Right? So now we've got our post. Let's add a published post.\u003C/p>\u003Cp>And, Kate, if you're watching this, I'm sorry. I'm just gonna steal this post entirely. Use it verbatim inside this platform. And I'm also gonna steal your thumbnail image. Don't sue me, please.\u003C/p>\u003Cp>Alright. So we've got our post. Great. Looks savvy. Okay.\u003C/p>\u003Cp>Now we need to tie these together. Right? We don't have it's just floating out in space. It's not inside a space, it's in outer space. Alright, so let's tie that together.\u003C/p>\u003Cp>We're going to use the relationship indirectus for that. We'll just go to our spaces and one space could have many posts. But I think a post always belongs to a single space. We wouldn't want posts in multiple spaces. So that is a good example of our one to many relationship.\u003C/p>\u003Cp>There's one space, many posts, and these are gonna be called posts. The foreign key inside our post table, we'll just call that space without the s, because it's just gonna be 1. Alright. So we'll save this. And I can go into advanced mode inside Directus just to to make sure everything that I want is getting created.\u003C/p>\u003Cp>And maybe we I don't know if we actually need that sort field or not, but it's fine. Let's do a post title. I probably could have went with the post name, because we had a space name and not a title. But, again, we're splitting hairs here. There's no right or wrong when when you're naming things.\u003C/p>\u003Cp>There's only shades of gray. Some things are bad, some things are not. Alright. So we've got our spaces, we've got our posts, we've got a relationship between them. Right?\u003C/p>\u003Cp>So if I go into my spaces now and I click add existing, I can add that existing post to our announcements space. I could save that. And then if I were to go to our discussion, I could potentially pull that in here, but because of that relationship, it would decouple those. Alright. So next we've got, what, comments?\u003C/p>\u003Cp>Posts could have multiple comments. And let's do date created. We really don't have a status on these, do we? Like, you know, if the post if the comment is edited, we'll we'll be able to tell by the user updated field or the date updated field. So there we go.\u003C/p>\u003Cp>We've got the comments content, content body, comment message. Yeah. Let's roll with message. That's fine. So now we've got our comments.\u003C/p>\u003Cp>Let's send that relationship to the post. So we are, again, going to do a another one to many relationship. Right? Because we have a post. One post has many con comments.\u003C/p>\u003Cp>Okay. So oh, actually, we're on comments here. Right? So we need the inverse relationship of that. We've got the many to 1, because we were on the comments.\u003C/p>\u003Cp>So the key here will be post. Our related collection is posts. I'm gonna open up advanced because I am going to add that corresponding field inside the posts collection. That's gonna be comments. Great.\u003C/p>\u003Cp>We can add our title there. Cool. Okay. So we've got comment, we've got a post. Now if we were to go, actually, let's just hop over to our post, make sure this is showing.\u003C/p>\u003Cp>Alright. Maybe we wanna show the space that this is attached to as well. Go into my post. Here's the post. There's the title.\u003C/p>\u003Cp>Here's the space. I can go in and add a comment. Really simple. New comment. Bada bing bada boom.\u003C/p>\u003Cp>Got a comment for that space. I can see it here, though we can't actually read it. It's just showing the UUID. So I can go back in and maybe we clean this up a little bit where we got our spaces, got our posts. And here in the display template for this, I could show the actual message.\u003C/p>\u003Cp>And if I wanted to, I could even get into the the weeds a bit here and show the user avatar. Is it gonna be the avatar or maybe the actual avatar thumbnail? Yeah. There we go. Looking nice.\u003C/p>\u003Cp>Cool. Let's see what that looks like. Inside our post, new comment. We don't have an avatar for for little old me, so let's solve that really quickly as well. Just do right.\u003C/p>\u003Cp>Right ahead. Where are you? Me. There we go. Cool.\u003C/p>\u003Cp>We'll go ahead. Alright. So I could see myself that I made a comment. We can see the space it's attached to. What what are we missing from our functionality?\u003C/p>\u003Cp>Right? Comments, likes, and we could do that. Post, comments, likes. I you know, do we even really wanna do likes on this? Yeah.\u003C/p>\u003Cp>I think this is fine. Right? Let's let's roll with this for now and get something happening on our front end. Let's build something cool. I'm sure I'm missing something.\u003C/p>\u003Cp>But oh, what if we had, like, threaded comets? Right? I don't know if the circle support that type of maneuver. Looks like it does. Right?\u003C/p>\u003Cp>So, I can also have recursive relationships. So this will be a good look at what's available inside Directus as well. So I could go in and create recursive relationships. Boom. Boom.\u003C/p>\u003Cp>Boom. Boom. Boom. And, you know, we we could take care of this on the front end. Or, you know, if I wanted to show that recursion, we could do it here.\u003C/p>\u003Cp>So we'll add a parent key, and then we'll just reference that same collection. And let's do, like, children. Sounds cool. Great. K.\u003C/p>\u003Cp>And as far as the interface, I could also do, like, the tree view, which would show, like, nested recursive items, as it says. But, maybe we don't drill down any more than one layer here. Cool. Alright. So now I can have a comment.\u003C/p>\u003Cp>I can have, a parent underneath that. Parent comment. Oh, did I get this backwards? Yeah. Maybe I goofed that up a little bit.\u003C/p>\u003Cp>Name these the wrong things. So children. Yeah. I did, didn't I? Let's just sort that out.\u003C/p>\u003Cp>Right? The should have been a mini to 1. Okay. So that's the parents, and that's this comments collection. And then we have our children.\u003C/p>\u003Cp>Didn't necessarily have to add that one, but cool. Okay. Now now we should be looking good. Right? So I can add all the children.\u003C/p>\u003Cp>This is a child comment. Cool. Alright. So we're setting up a lot of this. This is gonna look nice on the front end.\u003C/p>\u003Cp>Before we get into the front end, I'm also gonna go in and add a new role to the back end here. So I'll just go in, we'll call this a user. They probably don't need app access. Right? We're going to build our own front end for this.\u003C/p>\u003Cp>And then we will give them access to see all the comments, see all the posts, see all the spaces. You know, do we want them to be able to create comments? Yes. We'll let them create posts. Can they create spaces?\u003C/p>\u003Cp>No. Can they edit comments? Yes. Potentially. Can they edit posts?\u003C/p>\u003Cp>Yes. They could edit posts. But let's do a custom permission where user created has to equal their own ID. So user created equals current user dot ID, and I'm not sure if this is actually user created dot ID. Let's try it that way just to be sure.\u003C/p>\u003Cp>Alright. So now they can only edit their own posts. We could do the same for comments. And, you know, do we want to let them delete their posts? Yeah.\u003C/p>\u003Cp>Probably do. But cool. Alright. So let's check the timer. We're looking good.\u003C/p>\u003Cp>And now let's start building something. Right? This is not much of an app to look at at this point, but maybe we drag this over and start building. Cool. Alright.\u003C/p>\u003Cp>So on the index page, let's just take a look and and dissect what we've got here. We've got like a nav on the left, then we've got the actual space here on the right. And I think that is it's pretty static. Right? So let's use our Nuxt layouts for this.\u003C/p>\u003Cp>We've got a default layout. We could see that in action here. Do we wanna use layouts, or do we wanna actually do a page for this? Let's do a page. So I could use, like, a Nuxt child route for this.\u003C/p>\u003Cp>What do you call this? What do they call their URLs? Right? We load this thing up. It's just slash home.\u003C/p>\u003Cp>Right? Slash c. Okay. Oh, looks like they've got slash c. Okay.\u003C/p>\u003Cp>That's part of all of these. Right? So we we could do something similar where we have, pages a dot view vcomp ts. Yeah. I'll I'll usually end up doing something like app dot view.\u003C/p>\u003Cp>Okay. So we're gonna have 2 sections here. We've got, like, a left sidebar, and then we have the content on the right hand side. So we can, like, flex this. And on the left sidebar, They make the is the width dynamic?\u003C/p>\u003Cp>No. You can't change the width on theirs. Probably a good thing. Easier that way. So let's just say with 56, we'll do p g gray 50.\u003C/p>\u003Cp>And then we have the, what, main content. Right? Sounds great. Div. And here, we're gonna put this Nuxt page component.\u003C/p>\u003Cp>Right? Now let's see what we got. Right? If we go to pages slash a, What did I do? Why is this not working?\u003C/p>\u003Cp>Pages slash a. Come on. Refresh. Next page. Next page.\u003C/p>\u003Cp>Oh. Slasha. Okay. Slash local host/a/app. No.\u003C/p>\u003Cp>Slasha. Okay. Yeah. Cool. So we got nothing here.\u003C/p>\u003Cp>Right? Nothing here. There it is. There we go. Okay.\u003C/p>\u003Cp>So now, let's pick up our spaces. We're gonna render these over here. If I look at the Nuxt UI library, one of the things that I like here is they have this vertical navigation component. So we're just gonna use that. Right?\u003C/p>\u003Cp>Here's the structure for it. We can pull that in. Let's just drop it at the end of this. Copy these links just so we get our structure. And then we'll put vertical nav in the sidebar.\u003C/p>\u003Cp>But, obviously, you gotta delete these other ones. Okay. So we now we could see something going on. Maybe this needs to be height full. What is our default look here?\u003C/p>\u003Cp>High screen, maybe. Okay. Alright. So now we're getting that running the full gamut. We'll probably build in a little bit of padding here.\u003C/p>\u003Cp>P y 8. K. Maybe we're gonna add a logo at the top at some point. But now how do we actually get our data from Brectus into this particular instance, into this app. Alright.\u003C/p>\u003Cp>We wanna pick up these spaces dynamically. What we're gonna do in that case is call the Directus API. Alright. So we're gonna do we could use the use async data composable, especially if this is gonna be accessible on the front end, without authentication or anything. Maybe you want a server side render this if it is a space that has, you know, public data on it.\u003C/p>\u003Cp>Alright. So we're gonna use async data. We give this a key. Let's just call this, root. And I think this is the syntax.\u003C/p>\u003Cp>Alright. So I've got a composable setup in my Directus module. Where are you, mister Directus module? There we go. Use Directus, and here we will do this.\u003C/p>\u003Cp>We're going to call use Directus, and I'm gonna use the SDK that we have here. So I'm gonna read the items from our spaces collection. And, you know, I could add in, like, a some query params here or, some parameters to filter that out, but let's just take a look at our data. So I'm already seeing in the console, we've got a server 403 fetch error forbidden. Right?\u003C/p>\u003Cp>That is because we are not logged in. So how do we solve that? Right? Inside our Directus module that I've got set up, I've got a an auth composable that will log us in and fetch the user and all of that, and then store that as the user and inside use state. And I've even got in a middleware.\u003C/p>\u003Cp>Right? So we could add a middleware to this page, where if we're not logged in, it is going to send us to the authentication page. So, you know, if this was actually public data. Right? Maybe we needed to adjust our permission settings.\u003C/p>\u003Cp>Right? So in public, I could go in, maybe people can read all the comments. They could see the spaces. They'll probably still be able to read the see the spaces. Right?\u003C/p>\u003Cp>It will probably just be like the post that you would not be able to see unless they are authenticated. Alright. So here's one way we can set that up. Right? I can go into my item permissions and go to my space.\u003C/p>\u003Cp>And if this is private, if it's private, I'm going to restrict. So is private equals nothing. So if if private is null, they could see all those posts. But now if I refresh, I should remove that error. And then if I look at my app, We could drill in a couple layers here.\u003C/p>\u003Cp>Where's my page? Alright. So we can see the spaces that we're getting back here. Great. Okay.\u003C/p>\u003Cp>So we got our individual spaces. Now let's ditch these links. Alright. And just map these out. So our links are gonna be, let's say, the let's call these spaces.\u003C/p>\u003Cp>And we'll just do something like this where nav spaces equals computed. So we'll just grab this data. And we are going to map this out. Right? So we'll do spaces dot value dot map, and we got a space.\u003C/p>\u003Cp>And for each space, we are going to return what, label. So that's gonna be the space dot name. Do we have an icon for these? I we didn't have an icon, but we could potentially set one up. Or maybe we don't even have an icon.\u003C/p>\u003Cp>We'll just omit that. And then we have 2. Right? So 2 is gonna be slash, and let's use a template literal here. We'll do slash dot space spaces/space.slug.\u003C/p>\u003Cp>Cool. Alright. And now if I do something like this, let's just log this out to make sure we're getting that. Spaces. Nav.\u003C/p>\u003Cp>Why am I not getting that? Next page. Where's our app? Slash a. Where are you, baby?\u003C/p>\u003Cp>What are you doing? There it is. There's our component. Now spaces cannot read property of undefined. If spaces if no spaces maybe we do this.\u003C/p>\u003Cp>Data equals unref our spaces. No. We'll just do this. If spaces, no spaces, we'll return empty array. So let's fix our issues.\u003C/p>\u003Cp>What do we get here? So spaces is still undefined. What am I doing wrong? Rich, doing something wrong up here in the actual call. Let's take a look at the use async data.\u003C/p>\u003Cp>Oh, duh, gosh. I need more coffee. So await async data. Why is this not still need do I need to return this? There we go.\u003C/p>\u003Cp>Fingers crossed. Fingers crossed. Refresh. Okay. So we got our spaces.\u003C/p>\u003Cp>We're still getting undefined on our computer here. Unrefspaces.map. Alright. Why is nav spaces undefined? Space dot name.\u003C/p>\u003Cp>So if we look at our spaces here, we have a name. We have a slug for those. Spaces is an array. We can confirm that. Do we have it now?\u003C/p>\u003Cp>Nope. Okay. That's always the fun of doing this. Oh, nav spaces. Undefined.\u003C/p>\u003Cp>What am I doing wrong here? Good question. Let's look at the clock. 23 minutes. Alright.\u003C/p>\u003Cp>So it looks like I just forgot to return this, basically. I think that solves the problem. Okay. Yeah. So now I can actually see that.\u003C/p>\u003Cp>Right? Kind of a bonehead move, but there we go. Sometimes it happens. So now, we've got our spaces over here and we could even, let's have a look at this component. I don't know if it will actually return.\u003C/p>\u003Cp>You can use the default slot or an avatar slot, customize the avatar, or give it an icon. Yeah. What if we just give it a, like, a circle icon? Icon is circle. Let's use the material symbols, is there material symbols here?\u003C/p>\u003Cp>Maybe circle. Sorry, icon. We'll just search all these. Right? They're just like a nice circle icon we could use.\u003C/p>\u003Cp>Yeah. It's fine. This is the material circle icon. Let's try that for all of these different items. Icon.\u003C/p>\u003Cp>Does that work? No. We need to change the syntax with Nuxt UI. Yeah. It's not showing up.\u003C/p>\u003Cp>Alright. So we'll just ditch the icon for now. Cool. Alright. Let's get on with building this thing.\u003C/p>\u003Cp>Right? We've got our main page, now we've got our spaces. If I click on the space, I don't have anything showing up for the space. So we'll go in and create a new folder and then we're going to do a spaces ID dot view. Right?\u003C/p>\u003Cp>The other thing that I'm going to do here is create a new folder called a and drop spaces in there as well. And then I need to go back here and adjust this to where it says a or app or whatever it is. We're just kind of following their convention here. Alright. So we've got spaces.\u003C/p>\u003Cp>Id. We'll just create a View component and spaces. Alright. So we click here. Are we on the space?\u003C/p>\u003Cp>Yep. And we could prove that just by doing something like this where we do cons route equals use route and route. Paramsiddiscussion. Right. And we could probably call this slug.\u003C/p>\u003Cp>It might be a better way just to keep everything clean, tight. And then we refresh, we can see slug is discussion. Okay. So how do we actually use this? Again, we're just gonna go in and fetch the space and we'll use async data, and we'll do spaces.\u003C/p>\u003Cp>And, actually, do we need the we're we're getting the space slug. Yeah. So we'll probably fetch that. Let's call it spaces dash. If you don't provide a, like, key for async data, it will use the the actual print.\u003C/p>\u003Cp>It will use the the spot that it's at in the file, like, the the file and the line number and things like that. But we'll just give it an actual key here to use when it caches things. Alright. So as far as our data, we're gonna return, we're gonna use Directus, we're gonna read the items from spaces and we are going to filter this down. Right?\u003C/p>\u003Cp>So the filter is at the slug underscore equals route dot params dot slug, you know, I could filter this out a little bit, Prayms route. I don't even know if this will work. Equal slug. Returned, use directus, slug. Unexpected keyword return.\u003C/p>\u003Cp>Do I not need the return? Oh, wait. Use async data. Oh, I forgot my function there. Okay.\u003C/p>\u003Cp>So we're gonna return, use directus, and, oh, I got a period on my slug. Cool. Are we actually getting data there? Let's take a look at that component. Right?\u003C/p>\u003Cp>This is the slug, so we'll just use our view dev tools. Do we have our space? Yes. We do. And then inside this use async data call, let's back this out.\u003C/p>\u003Cp>Right? Where are you? I can do is it transform? No. They have a a set of options.\u003C/p>\u003Cp>Transform, data, data 0. Let's see what that does. Now that should grab my single space. So the spaces are space because when I read items, I'm always returning an array. But using this transform on the async data call, I can transform that data and just pick up the first item from that array.\u003C/p>\u003Cp>So there's my space. I don't see any posts for discussion, but if we go to announcements alright. Do we see any posts there? Yeah. That's great.\u003C/p>\u003Cp>Okay. So, let's take a look at what we've got here for circle. We've got our discussion or our spaces over here, and then we have the discussion here. Alright. Cool.\u003C/p>\u003Cp>So we will add a header. It's got a cover image on it, basically. Let's try to find one where we did have some discussion. Right? Courses, engagement, Contest guidelines.\u003C/p>\u003Cp>Okay. Alright. So then we click into a post, and that is the assuming that's a space. Events new. I click into that.\u003C/p>\u003Cp>Events new. That's the slug for the post, I assume. Alright. So let's render out the header image for the space. Source is gonna be space dot cover underscore image.\u003C/p>\u003Cp>We'll make this full width. Maybe a height of 32. Object cover. See what we've got here. Just flip back.\u003C/p>\u003Cp>Input must be a string. Okay. So if we don't have the space dot cover image, space dot cover image, let's not try to render it. Is that the problem? Discussion.\u003C/p>\u003Cp>Okay. So I'm trying to get the image here, but what's happening? Right? Why are we not getting the image? It is because we are getting a 403 forbidden.\u003C/p>\u003Cp>We'll go into Directus here, go back to our access control. We have not allowed files to be shared. Okay? So there's our actual item. That's great.\u003C/p>\u003Cp>This is gonna be flex Flex 1. With full. Yeah. Okay. Alright.\u003C/p>\u003Cp>So we got our header. Let's just keep moving. Then we have our post detail. Right? I've got a v text component here that this will render, like, paragraph text.\u003C/p>\u003Cp>But, if we're gonna do something like the rich text for the post, So let's look at posts. We've got, like, this post content here. So we're great for a title. Alright. So let's just do, like, a h 2 or maybe an h one for the post.\u003C/p>\u003Cp>Not sure. Let's just do h two. Oh, we're not even showing the actual post. Right? Let's do an h one because we wanna show the space name.\u003C/p>\u003Cp>Alright. So there's the space name. It gives us a little padding. Pxpy6. Okay.\u003C/p>\u003Cp>Make this bold. Make it a little larger. 2 XL. Alright. So there we have that.\u003C/p>\u003Cp>Let's dive into the actual posts. Right? So 2 ways I could go about this. I could get all of the posts in a single call with Directus, or I could fetch these separately as well. In this case, let's just render some of these posts out.\u003C/p>\u003Cp>So we will go into our query here, and we'll do fields. Alright. So if I wanted to do, like, some cards so let's just back up and take a look at circle here. Right? We've got a couple of cards here that are showing up.\u003C/p>\u003Cp>Let's do the ucard, which is inside that Nuxt UI library. We're gonna do a post in space dot post. And Get a key. Key is gonna be post dot ID. And And then let's just render out post dot title.\u003C/p>\u003Cp>Oh, and that's not building because I did not close that off. Right. But now I'm not seeing anything. If I go to announcements, hey, I've got a card here. Nothing is rendering.\u003C/p>\u003Cp>And that's because if I take a look at my data, right, where are you, data. So inside this slug, I've got the space, we've got some posts here, but it's just returning an actual ID. How do I grab the actual post for this? So I go into fields. I could grab all the root level fields like this.\u003C/p>\u003Cp>And with the SDK, I can do the the TypeScript safe syntax is is actually specifying this out as an object. But for the sake of this, maybe I do, like, a double wildcard here. And now we could see, if I open this up, we can see the space, we get the post. It is fetching all of the content for that specific post. Right?\u003C/p>\u003Cp>Great. Looks like we've got a card. How do these cards look? Okay. So we got a header on the card.\u003C/p>\u003Cp>That's the the post image. Alright. So we got template. Be the card header. Then we just have a regular slot.\u003C/p>\u003Cp>P. Oh, where are you? Mister p tag. And here, this will be the header. We've got Nuxt Image.\u003C/p>\u003Cp>That'll be the v if post cover underscore image. The source would be post dot cover underscore image. Let me just close that up, see what we get. Okay. Now we're getting somewhere.\u003C/p>\u003Cp>Great. This is actually gonna be a NuxLink. Right? Do we wrap the entire card? How are they doing it here?\u003C/p>\u003Cp>Looks like the the entire image, and then I can click over here. Right? So we wrap the image in a Nuxt link, Nuxt link. For our spaces, we're gonna do this will be the post. We'll add, and then we'll do something like this where we have index.\u003C/p>\u003Cp>I think this will give us the routing that we want. Alright. What else was I missing? The actual Nuxt link. Right?\u003C/p>\u003Cp>2 a slash, we got what the space well, actually, this is gonna be the slug. Alright. Well, we've got the slug up there. And then we have post dot slug. Post dot slug.\u003C/p>\u003Cp>Yeah. Cool. And there's our Nuxt link. I will just copy this here. Run that one more time.\u003C/p>\u003Cp>Run it back. And does that give us where we want to go? Except it doesn't, does it? Okay. Okay.\u003C/p>\u003Cp>Yeah. No. Now we're navigating where we wanna go. Why do we not have a slug? Do we not have a slug on the post?\u003C/p>\u003Cp>We don't. That's why it's not showing up. Alright. So let's quickly add a slug to these posts. And, you know, we could even create a, an automation inside Directus that would do this for us.\u003C/p>\u003Cp>And I'll just quickly add a test log. Okay. So we go there. Test log is not showing up. Page not found.\u003C/p>\u003Cp>Why is that? Because we're not doing the actual right. So we got spaces slash spaces. K. Now we go back.\u003C/p>\u003Cp>We try it again. Right? Go back. Refresh, maybe. K.\u003C/p>\u003Cp>A spaces slash announcement post. Oh, the Alright. This needs to be an actual we need a folder for that. Right? What's the best way to set this up?\u003C/p>\u003Cp>Okay. So spaces actually becomes this. Right? This is the space, and then this is the index route for that space. This becomes anywhere that we're referencing slug becomes the actual space.\u003C/p>\u003Cp>Brand space space. Oh, let's not destructure that space. Slug. Does that work? Spaces, slug, Space.\u003C/p>\u003Cp>Space. Space. Space. What are we what are we missing? What are we missing?\u003C/p>\u003Cp>Let's just go back. Alright. Slug. Okay. So now I've got the space.\u003C/p>\u003Cp>Right? Let's just log the params out here. Params. Why can't I like any ears? Params, slug, spaces announcements.\u003C/p>\u003Cp>Oh, it has to do with the URL structure now. Right? We've changed that. So we go back to our tag here. We don't really need this.\u003C/p>\u003Cp>Okay. So at least now we're getting an error. And what else do we have here? Index dot view slug space slug. Give it to me.\u003C/p>\u003Cp>Input string space dot cover image. Oh. What am I doing wrong here? So there's our announcements. Okay.\u003C/p>\u003Cp>It's because we don't have any. This would be v if space dot post dot length greater than 0. Solve the the issues. It doesn't like that. Vcard.\u003C/p>\u003Cp>Vfpost.coverimage.source.post.coverimagespace. Why does it unlike the unlike the discussion page? No worries, though. So we change the routing there. Alright.\u003C/p>\u003Cp>So now we are now we're getting somewhere. Right? We've got the routing the way that they do it. And then inside this post, we will actually fetch the individual post as well. Alright.\u003C/p>\u003Cp>So we'll come here. Let's just call this post so we can render that out. The data will be post. We are going to fetch the post dot dash slug. And instead of spaces, we're gonna do post.\u003C/p>\u003Cp>And in this case, this one is actually called post. Alright. What else do we need? It's gonna grab our comments as well. Alright.\u003C/p>\u003Cp>Boom. Okay. So now we can see our post. Looking great. If we go back, we'll grab the header image.\u003C/p>\u003Cp>K. Post dot cover image. Do we have a cover image for that post? Yes. We do.\u003C/p>\u003Cp>Why are we not sorting through that post dot cover image? Input must be a string. Oh, because we're fetching all of the data. Let's just delete that. That's probably why.\u003C/p>\u003Cp>Okay. So there's our cover image. Let's do a div. We're gonna do VHtml. This is gonna render out our actual content.\u003C/p>\u003Cp>So post dot contents. And Telwin has some really nice typography styles already preset for you, and we are going to just use those. Right? Pros. Missing our intag.\u003C/p>\u003Cp>Alright. Let's wrap this again. Div. We've got p x 4. Give us some padding.\u003C/p>\u003Cp>K. There's our post. Let's get into the comments. Right? How do we get down to the comments?\u003C/p>\u003Cp>We'll do just give this a little breathing room. P x a p y 8. 6 is not nearly enough padding. I definitely need to go in and, update the the actual mobile views for this as well. And then we have our comments section.\u003C/p>\u003Cp>How we doing on time? 55 seconds. Man, this one has been a little bit of a struggle. A little more to it than what I thought originally. Also operating on not enough coffee.\u003C/p>\u003Cp>So it's it's pretty painfully obvious at this point. Not going to get to any of the comments. So if we look at our functionality, right, we managed to get here. Users can log in. Did we even get there?\u003C/p>\u003Cp>We actually did, because it was already baked in, but we didn't we didn't really mess with that. Alright? There's a auth login page where I could actually log in to this thing. Users create posts and add comments to those posts, create and manage events. Man, like, we did not do so great on this one.\u003C/p>\u003Cp>So, sometimes you fail. Circle is a really cool app. You know, recreating that functionality, there's a lot baked into it. So, you know, hey. What do we give this one?\u003C/p>\u003Cp>Like, Cisco and Evert would probably give this one 2 thumbs down, but that's the way it rolls. You know, we've learned a lot about the different data models and how to use the different relationships inside Directus. So let's chalk it up as a a tie, maybe. I don't know. That's it for this episode of 100 apps, 100 hours.\u003C/p>\u003Cp>Thanks for joining me. We'll see you on the next one.\u003C/p>","Hi guys. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus. And today, we will be building a community platform. Now, this is one that is near and dear to my heart because I have built a community for sign and print shop owners myself. We use Facebook Groups for that. Not the best community platform, but it is free and everybody already has a Facebook account. But in just a quick look at the research for this episode, There's quite a few different tools out there that I located, Circle being one of the more popular ones. It's not exactly a forum, it's not exactly a course, it's not exactly chat, it's all of those things. So the pieces that we're really going to dive into today are this concept that they have of spaces. So each one of these items in the left navigation is a space. A space can be, either private or public. So I can see here some of the circle experts and this is their their own example community. Right? They do have courses in here. We've already done an episode on LMS, so we won't really touch on that. But, if we get into this, we could see what's new in the product. So this is an update we're gonna post. We've got some comments down at the bottom, I'm assuming, and you had to be logged in to comment. So how do we kinda recreate some of this functionality? It looks like they have weekly events here, but we're probably not seeing those. And maybe here's something similar. So anyway, that's what we're building. If you, have watched any of the other episodes, you probably already know the rules. We've got 60 minutes to plan and build, no more, no less. And the second rule, which is not really a rule, is use whatever you have at your disposal. So, any of the tools that are out there, AI, Tailwind CSS is one of my favorite ones. We're going to be building this platform on Directus as well. That'll be our back end. You could probably even actually build the entire thing inside Directus, but we'll probably go for a custom UI on the front end. Alright. So, sounds great. Let's rock and roll. So we'll start the 60 minute timer and away we go. So anytime I build an app, I always love to just kinda flesh out the functionality ahead of time. So if we're looking at this specific app, maybe I'll just pull up a text box inside Figma here. Here's the functionality that we really want out of this app. Right? We want to create and manage different spaces. Spaces for a how do I can I constrain this? Yeah. There we go. For a community. Maybe we have events as well that we're going to list. List and create and manage events. Events. We need to be able to log in. Users can log in. Create posts. Right? Is that what we're going to call that? Maybe a thread? We'll get into it. Right? We'll create some posts and add comments to those posts. That feels like a a pretty good chunk of functionality. I don't know if we'll get to all that or not. That's, like, actually building the UI for that, could be fun. Right? Alright. So let's just, like, flesh out our our actual data model here as well. So again, I'm not the savviest Figma user, but I try. I'm a tried. Alright. So what do we have? We've got spaces. So those are gonna be our little homes for all this different stuff, this different type of content. Right? Then we've probably got, this is where we get into the naming. Right? Is it a a thread? Is this a what what are these things called? Are these posts? Yeah. Let's go with that. Right? This is a post. And a post has some comments. Probably has a probably has, like, a post type, I'm assuming. K. Let's look at what else we've got here. Share your wins. Log in. Yeah. Okay. So these are different posts. You can upload images to a post. You can add comments. You can like. So we probably got some likes in there. And I don't know that we'll get to that either. But, what else do we have? We have users. That's gonna be taken care of by Directus because it gives us that out of the box, which is nice. That'll be our Directus users collection. And what else do we need for a function community? Right. Are events gonna be a separate thing than posts? I don't know. Let's we'll try it out. Cool. Let's just add that to the end here in case we we don't have time to get to it. Alright. So this feels pretty good. We're closing in on 4 minutes or so in the planning stage. Alright. So what do we have set up? We have, we got Directus going here. I've got a Directus instance that is totally blank, so I just reload. You'll see there are 0 collections. And on the front end, I've got a starter Nuxt application that just basically has the Directus module or the Directus plug in already preconfigured. We're using the SDK to communicate with the Directus instance, but I've just got Directus spun up on a local Docker instance. So, really fancy app here. We've got Tailwind. We're using Nuxt UI as well on that front end. So got quite a few things going on there. Alright. So first and foremost, we've got spaces. Let's go ahead and create our data model. Directus makes this super easy. We'll just go into our blank instance. We'll create our first collection. We're going to call it spaces. So those are the different spaces here. We'll generate the UID. We'll probably have a a status for the space. What's the sort order for the spaces? Who created these? When were they created? That's all pretty standard. Let's just take a look at the network requests on this to see if we can actually pick up some of their data model. Alright. Space groups. Alright. There's the circle community. Allow members to spaces. What else do we have? There's a slug for the space. So that's, that's a handy piece of information to have. Spaces, discussion. Okay. So we've got a cover image. Cover image URL. And this is via their API, so it doesn't necessarily represent the underlying database structure, but this data is coming from somewhere. So we're gonna have a name for the space. Name for the space. That's great. Alright. Do they have a description? Space description. Space name. We're definitely gonna have a slug, so that's great. We'll roll with that. Advanced field creation mode and, when I need something like a slug or a value that URL safe, if I go into advanced mode, I have Slugify available. So that will automatically transform the input as I type. Lock screen heading. I don't know, man. Let's just give it a description. And let's keep that non WYSIWYG. And last but not least, for our space, spaces, Let's add a cover image. So we've got our cover image. Right? And let's see if they had do they have a type of space? Space, slug. Visibility, post type is basic. I I wanna say there's probably some setting in here to control what these things look like, whether this is like a discussion or this is an event or a course. As we're fetching these, okay, that's a course. Internal spaces API. Maybe a type. Is there a type? Course type. Type is a banner. K. Those are yeah. No. I I don't wanna waste time on it. Right? Cool. So status, we'll just leave blank for now. Is this published or is it available? And then let's say let's add 1, like, is private. This will be a toggle. So the Boolean values Boolean value, I always used to struggle with that. That's what we'll save in the database. Is this space private or not? And then we can do a little cleanup here on our fields. So name, slug, description, maybe we pop that right beside there. And status wise, let's just roll with it. Right? So we go in, we create our first space. Maybe we wanna call this discussion, and that'll be at the slug for that will be discussion. Here's where we discuss stuff. Great. Is it private? Yes. Let's make it private. And then we will let's upload a cover image. Right? Unsplash. Let's just search for discussion. Okay. Images that are not seem to be working. Okay. Maybe some type of Internet issue. We're working here locally, anyway. Let's just grab from Circle. Right? We're already working with this specific thing. Do they have discussion? Where's the best practices for engagement? Yeah. Fine. Let's roll with it. Cool. Alright. So now we've got our first space. What else do we need to add to this? Right? So we've got posts for the spaces. Then each one of those posts could have comments, potentially. Right? So, let's just go in and create another. We'll just call it announcements as the next space. Announcements, read only channel for important announcements. Very fancy. Important announcements. Alright. So that is not private. Let's make both of these published just to to have it. Great. Okay, so we'll go back into our settings and let's add the next item. Let's add posts. We use the UUID. Do we have a status for the post? Yeah. Probably. Date created, date updated, is there a sort for the post? Do we wanna control the sort order on those? Yeah. Maybe potentially you do. Alright. So if we take a look at circle here. Let's dissect one of these posts. Right? So a post has a title, looks like. Also has a user that created the post. So Directus has taken care of that for us out of the box. We probably got a date that this was created. We've got some rich text for the post. So that'll be the post content, maybe? Post body? You do that. Let's just keep it as content. It seems pretty straightforward. Naming stuff is always the the hardest part about development. What else do we have? We got that cover image for the post. Right? Cover image for our post so that, you know, if we want to upload a cover image, we can. Sweet. Looks great. Let's go in and just create one of those posts. Right? So now we've got our post. Let's add a published post. And, Kate, if you're watching this, I'm sorry. I'm just gonna steal this post entirely. Use it verbatim inside this platform. And I'm also gonna steal your thumbnail image. Don't sue me, please. Alright. So we've got our post. Great. Looks savvy. Okay. Now we need to tie these together. Right? We don't have it's just floating out in space. It's not inside a space, it's in outer space. Alright, so let's tie that together. We're going to use the relationship indirectus for that. We'll just go to our spaces and one space could have many posts. But I think a post always belongs to a single space. We wouldn't want posts in multiple spaces. So that is a good example of our one to many relationship. There's one space, many posts, and these are gonna be called posts. The foreign key inside our post table, we'll just call that space without the s, because it's just gonna be 1. Alright. So we'll save this. And I can go into advanced mode inside Directus just to to make sure everything that I want is getting created. And maybe we I don't know if we actually need that sort field or not, but it's fine. Let's do a post title. I probably could have went with the post name, because we had a space name and not a title. But, again, we're splitting hairs here. There's no right or wrong when when you're naming things. There's only shades of gray. Some things are bad, some things are not. Alright. So we've got our spaces, we've got our posts, we've got a relationship between them. Right? So if I go into my spaces now and I click add existing, I can add that existing post to our announcements space. I could save that. And then if I were to go to our discussion, I could potentially pull that in here, but because of that relationship, it would decouple those. Alright. So next we've got, what, comments? Posts could have multiple comments. And let's do date created. We really don't have a status on these, do we? Like, you know, if the post if the comment is edited, we'll we'll be able to tell by the user updated field or the date updated field. So there we go. We've got the comments content, content body, comment message. Yeah. Let's roll with message. That's fine. So now we've got our comments. Let's send that relationship to the post. So we are, again, going to do a another one to many relationship. Right? Because we have a post. One post has many con comments. Okay. So oh, actually, we're on comments here. Right? So we need the inverse relationship of that. We've got the many to 1, because we were on the comments. So the key here will be post. Our related collection is posts. I'm gonna open up advanced because I am going to add that corresponding field inside the posts collection. That's gonna be comments. Great. We can add our title there. Cool. Okay. So we've got comment, we've got a post. Now if we were to go, actually, let's just hop over to our post, make sure this is showing. Alright. Maybe we wanna show the space that this is attached to as well. Go into my post. Here's the post. There's the title. Here's the space. I can go in and add a comment. Really simple. New comment. Bada bing bada boom. Got a comment for that space. I can see it here, though we can't actually read it. It's just showing the UUID. So I can go back in and maybe we clean this up a little bit where we got our spaces, got our posts. And here in the display template for this, I could show the actual message. And if I wanted to, I could even get into the the weeds a bit here and show the user avatar. Is it gonna be the avatar or maybe the actual avatar thumbnail? Yeah. There we go. Looking nice. Cool. Let's see what that looks like. Inside our post, new comment. We don't have an avatar for for little old me, so let's solve that really quickly as well. Just do right. Right ahead. Where are you? Me. There we go. Cool. We'll go ahead. Alright. So I could see myself that I made a comment. We can see the space it's attached to. What what are we missing from our functionality? Right? Comments, likes, and we could do that. Post, comments, likes. I you know, do we even really wanna do likes on this? Yeah. I think this is fine. Right? Let's let's roll with this for now and get something happening on our front end. Let's build something cool. I'm sure I'm missing something. But oh, what if we had, like, threaded comets? Right? I don't know if the circle support that type of maneuver. Looks like it does. Right? So, I can also have recursive relationships. So this will be a good look at what's available inside Directus as well. So I could go in and create recursive relationships. Boom. Boom. Boom. Boom. Boom. And, you know, we we could take care of this on the front end. Or, you know, if I wanted to show that recursion, we could do it here. So we'll add a parent key, and then we'll just reference that same collection. And let's do, like, children. Sounds cool. Great. K. And as far as the interface, I could also do, like, the tree view, which would show, like, nested recursive items, as it says. But, maybe we don't drill down any more than one layer here. Cool. Alright. So now I can have a comment. I can have, a parent underneath that. Parent comment. Oh, did I get this backwards? Yeah. Maybe I goofed that up a little bit. Name these the wrong things. So children. Yeah. I did, didn't I? Let's just sort that out. Right? The should have been a mini to 1. Okay. So that's the parents, and that's this comments collection. And then we have our children. Didn't necessarily have to add that one, but cool. Okay. Now now we should be looking good. Right? So I can add all the children. This is a child comment. Cool. Alright. So we're setting up a lot of this. This is gonna look nice on the front end. Before we get into the front end, I'm also gonna go in and add a new role to the back end here. So I'll just go in, we'll call this a user. They probably don't need app access. Right? We're going to build our own front end for this. And then we will give them access to see all the comments, see all the posts, see all the spaces. You know, do we want them to be able to create comments? Yes. We'll let them create posts. Can they create spaces? No. Can they edit comments? Yes. Potentially. Can they edit posts? Yes. They could edit posts. But let's do a custom permission where user created has to equal their own ID. So user created equals current user dot ID, and I'm not sure if this is actually user created dot ID. Let's try it that way just to be sure. Alright. So now they can only edit their own posts. We could do the same for comments. And, you know, do we want to let them delete their posts? Yeah. Probably do. But cool. Alright. So let's check the timer. We're looking good. And now let's start building something. Right? This is not much of an app to look at at this point, but maybe we drag this over and start building. Cool. Alright. So on the index page, let's just take a look and and dissect what we've got here. We've got like a nav on the left, then we've got the actual space here on the right. And I think that is it's pretty static. Right? So let's use our Nuxt layouts for this. We've got a default layout. We could see that in action here. Do we wanna use layouts, or do we wanna actually do a page for this? Let's do a page. So I could use, like, a Nuxt child route for this. What do you call this? What do they call their URLs? Right? We load this thing up. It's just slash home. Right? Slash c. Okay. Oh, looks like they've got slash c. Okay. That's part of all of these. Right? So we we could do something similar where we have, pages a dot view vcomp ts. Yeah. I'll I'll usually end up doing something like app dot view. Okay. So we're gonna have 2 sections here. We've got, like, a left sidebar, and then we have the content on the right hand side. So we can, like, flex this. And on the left sidebar, They make the is the width dynamic? No. You can't change the width on theirs. Probably a good thing. Easier that way. So let's just say with 56, we'll do p g gray 50. And then we have the, what, main content. Right? Sounds great. Div. And here, we're gonna put this Nuxt page component. Right? Now let's see what we got. Right? If we go to pages slash a, What did I do? Why is this not working? Pages slash a. Come on. Refresh. Next page. Next page. Oh. Slasha. Okay. Slash local host/a/app. No. Slasha. Okay. Yeah. Cool. So we got nothing here. Right? Nothing here. There it is. There we go. Okay. So now, let's pick up our spaces. We're gonna render these over here. If I look at the Nuxt UI library, one of the things that I like here is they have this vertical navigation component. So we're just gonna use that. Right? Here's the structure for it. We can pull that in. Let's just drop it at the end of this. Copy these links just so we get our structure. And then we'll put vertical nav in the sidebar. But, obviously, you gotta delete these other ones. Okay. So we now we could see something going on. Maybe this needs to be height full. What is our default look here? High screen, maybe. Okay. Alright. So now we're getting that running the full gamut. We'll probably build in a little bit of padding here. P y 8. K. Maybe we're gonna add a logo at the top at some point. But now how do we actually get our data from Brectus into this particular instance, into this app. Alright. We wanna pick up these spaces dynamically. What we're gonna do in that case is call the Directus API. Alright. So we're gonna do we could use the use async data composable, especially if this is gonna be accessible on the front end, without authentication or anything. Maybe you want a server side render this if it is a space that has, you know, public data on it. Alright. So we're gonna use async data. We give this a key. Let's just call this, root. And I think this is the syntax. Alright. So I've got a composable setup in my Directus module. Where are you, mister Directus module? There we go. Use Directus, and here we will do this. We're going to call use Directus, and I'm gonna use the SDK that we have here. So I'm gonna read the items from our spaces collection. And, you know, I could add in, like, a some query params here or, some parameters to filter that out, but let's just take a look at our data. So I'm already seeing in the console, we've got a server 403 fetch error forbidden. Right? That is because we are not logged in. So how do we solve that? Right? Inside our Directus module that I've got set up, I've got a an auth composable that will log us in and fetch the user and all of that, and then store that as the user and inside use state. And I've even got in a middleware. Right? So we could add a middleware to this page, where if we're not logged in, it is going to send us to the authentication page. So, you know, if this was actually public data. Right? Maybe we needed to adjust our permission settings. Right? So in public, I could go in, maybe people can read all the comments. They could see the spaces. They'll probably still be able to read the see the spaces. Right? It will probably just be like the post that you would not be able to see unless they are authenticated. Alright. So here's one way we can set that up. Right? I can go into my item permissions and go to my space. And if this is private, if it's private, I'm going to restrict. So is private equals nothing. So if if private is null, they could see all those posts. But now if I refresh, I should remove that error. And then if I look at my app, We could drill in a couple layers here. Where's my page? Alright. So we can see the spaces that we're getting back here. Great. Okay. So we got our individual spaces. Now let's ditch these links. Alright. And just map these out. So our links are gonna be, let's say, the let's call these spaces. And we'll just do something like this where nav spaces equals computed. So we'll just grab this data. And we are going to map this out. Right? So we'll do spaces dot value dot map, and we got a space. And for each space, we are going to return what, label. So that's gonna be the space dot name. Do we have an icon for these? I we didn't have an icon, but we could potentially set one up. Or maybe we don't even have an icon. We'll just omit that. And then we have 2. Right? So 2 is gonna be slash, and let's use a template literal here. We'll do slash dot space spaces/space.slug. Cool. Alright. And now if I do something like this, let's just log this out to make sure we're getting that. Spaces. Nav. Why am I not getting that? Next page. Where's our app? Slash a. Where are you, baby? What are you doing? There it is. There's our component. Now spaces cannot read property of undefined. If spaces if no spaces maybe we do this. Data equals unref our spaces. No. We'll just do this. If spaces, no spaces, we'll return empty array. So let's fix our issues. What do we get here? So spaces is still undefined. What am I doing wrong? Rich, doing something wrong up here in the actual call. Let's take a look at the use async data. Oh, duh, gosh. I need more coffee. So await async data. Why is this not still need do I need to return this? There we go. Fingers crossed. Fingers crossed. Refresh. Okay. So we got our spaces. We're still getting undefined on our computer here. Unrefspaces.map. Alright. Why is nav spaces undefined? Space dot name. So if we look at our spaces here, we have a name. We have a slug for those. Spaces is an array. We can confirm that. Do we have it now? Nope. Okay. That's always the fun of doing this. Oh, nav spaces. Undefined. What am I doing wrong here? Good question. Let's look at the clock. 23 minutes. Alright. So it looks like I just forgot to return this, basically. I think that solves the problem. Okay. Yeah. So now I can actually see that. Right? Kind of a bonehead move, but there we go. Sometimes it happens. So now, we've got our spaces over here and we could even, let's have a look at this component. I don't know if it will actually return. You can use the default slot or an avatar slot, customize the avatar, or give it an icon. Yeah. What if we just give it a, like, a circle icon? Icon is circle. Let's use the material symbols, is there material symbols here? Maybe circle. Sorry, icon. We'll just search all these. Right? They're just like a nice circle icon we could use. Yeah. It's fine. This is the material circle icon. Let's try that for all of these different items. Icon. Does that work? No. We need to change the syntax with Nuxt UI. Yeah. It's not showing up. Alright. So we'll just ditch the icon for now. Cool. Alright. Let's get on with building this thing. Right? We've got our main page, now we've got our spaces. If I click on the space, I don't have anything showing up for the space. So we'll go in and create a new folder and then we're going to do a spaces ID dot view. Right? The other thing that I'm going to do here is create a new folder called a and drop spaces in there as well. And then I need to go back here and adjust this to where it says a or app or whatever it is. We're just kind of following their convention here. Alright. So we've got spaces. Id. We'll just create a View component and spaces. Alright. So we click here. Are we on the space? Yep. And we could prove that just by doing something like this where we do cons route equals use route and route. Paramsiddiscussion. Right. And we could probably call this slug. It might be a better way just to keep everything clean, tight. And then we refresh, we can see slug is discussion. Okay. So how do we actually use this? Again, we're just gonna go in and fetch the space and we'll use async data, and we'll do spaces. And, actually, do we need the we're we're getting the space slug. Yeah. So we'll probably fetch that. Let's call it spaces dash. If you don't provide a, like, key for async data, it will use the the actual print. It will use the the spot that it's at in the file, like, the the file and the line number and things like that. But we'll just give it an actual key here to use when it caches things. Alright. So as far as our data, we're gonna return, we're gonna use Directus, we're gonna read the items from spaces and we are going to filter this down. Right? So the filter is at the slug underscore equals route dot params dot slug, you know, I could filter this out a little bit, Prayms route. I don't even know if this will work. Equal slug. Returned, use directus, slug. Unexpected keyword return. Do I not need the return? Oh, wait. Use async data. Oh, I forgot my function there. Okay. So we're gonna return, use directus, and, oh, I got a period on my slug. Cool. Are we actually getting data there? Let's take a look at that component. Right? This is the slug, so we'll just use our view dev tools. Do we have our space? Yes. We do. And then inside this use async data call, let's back this out. Right? Where are you? I can do is it transform? No. They have a a set of options. Transform, data, data 0. Let's see what that does. Now that should grab my single space. So the spaces are space because when I read items, I'm always returning an array. But using this transform on the async data call, I can transform that data and just pick up the first item from that array. So there's my space. I don't see any posts for discussion, but if we go to announcements alright. Do we see any posts there? Yeah. That's great. Okay. So, let's take a look at what we've got here for circle. We've got our discussion or our spaces over here, and then we have the discussion here. Alright. Cool. So we will add a header. It's got a cover image on it, basically. Let's try to find one where we did have some discussion. Right? Courses, engagement, Contest guidelines. Okay. Alright. So then we click into a post, and that is the assuming that's a space. Events new. I click into that. Events new. That's the slug for the post, I assume. Alright. So let's render out the header image for the space. Source is gonna be space dot cover underscore image. We'll make this full width. Maybe a height of 32. Object cover. See what we've got here. Just flip back. Input must be a string. Okay. So if we don't have the space dot cover image, space dot cover image, let's not try to render it. Is that the problem? Discussion. Okay. So I'm trying to get the image here, but what's happening? Right? Why are we not getting the image? It is because we are getting a 403 forbidden. We'll go into Directus here, go back to our access control. We have not allowed files to be shared. Okay? So there's our actual item. That's great. This is gonna be flex Flex 1. With full. Yeah. Okay. Alright. So we got our header. Let's just keep moving. Then we have our post detail. Right? I've got a v text component here that this will render, like, paragraph text. But, if we're gonna do something like the rich text for the post, So let's look at posts. We've got, like, this post content here. So we're great for a title. Alright. So let's just do, like, a h 2 or maybe an h one for the post. Not sure. Let's just do h two. Oh, we're not even showing the actual post. Right? Let's do an h one because we wanna show the space name. Alright. So there's the space name. It gives us a little padding. Pxpy6. Okay. Make this bold. Make it a little larger. 2 XL. Alright. So there we have that. Let's dive into the actual posts. Right? So 2 ways I could go about this. I could get all of the posts in a single call with Directus, or I could fetch these separately as well. In this case, let's just render some of these posts out. So we will go into our query here, and we'll do fields. Alright. So if I wanted to do, like, some cards so let's just back up and take a look at circle here. Right? We've got a couple of cards here that are showing up. Let's do the ucard, which is inside that Nuxt UI library. We're gonna do a post in space dot post. And Get a key. Key is gonna be post dot ID. And And then let's just render out post dot title. Oh, and that's not building because I did not close that off. Right. But now I'm not seeing anything. If I go to announcements, hey, I've got a card here. Nothing is rendering. And that's because if I take a look at my data, right, where are you, data. So inside this slug, I've got the space, we've got some posts here, but it's just returning an actual ID. How do I grab the actual post for this? So I go into fields. I could grab all the root level fields like this. And with the SDK, I can do the the TypeScript safe syntax is is actually specifying this out as an object. But for the sake of this, maybe I do, like, a double wildcard here. And now we could see, if I open this up, we can see the space, we get the post. It is fetching all of the content for that specific post. Right? Great. Looks like we've got a card. How do these cards look? Okay. So we got a header on the card. That's the the post image. Alright. So we got template. Be the card header. Then we just have a regular slot. P. Oh, where are you? Mister p tag. And here, this will be the header. We've got Nuxt Image. That'll be the v if post cover underscore image. The source would be post dot cover underscore image. Let me just close that up, see what we get. Okay. Now we're getting somewhere. Great. This is actually gonna be a NuxLink. Right? Do we wrap the entire card? How are they doing it here? Looks like the the entire image, and then I can click over here. Right? So we wrap the image in a Nuxt link, Nuxt link. For our spaces, we're gonna do this will be the post. We'll add, and then we'll do something like this where we have index. I think this will give us the routing that we want. Alright. What else was I missing? The actual Nuxt link. Right? 2 a slash, we got what the space well, actually, this is gonna be the slug. Alright. Well, we've got the slug up there. And then we have post dot slug. Post dot slug. Yeah. Cool. And there's our Nuxt link. I will just copy this here. Run that one more time. Run it back. And does that give us where we want to go? Except it doesn't, does it? Okay. Okay. Yeah. No. Now we're navigating where we wanna go. Why do we not have a slug? Do we not have a slug on the post? We don't. That's why it's not showing up. Alright. So let's quickly add a slug to these posts. And, you know, we could even create a, an automation inside Directus that would do this for us. And I'll just quickly add a test log. Okay. So we go there. Test log is not showing up. Page not found. Why is that? Because we're not doing the actual right. So we got spaces slash spaces. K. Now we go back. We try it again. Right? Go back. Refresh, maybe. K. A spaces slash announcement post. Oh, the Alright. This needs to be an actual we need a folder for that. Right? What's the best way to set this up? Okay. So spaces actually becomes this. Right? This is the space, and then this is the index route for that space. This becomes anywhere that we're referencing slug becomes the actual space. Brand space space. Oh, let's not destructure that space. Slug. Does that work? Spaces, slug, Space. Space. Space. Space. What are we what are we missing? What are we missing? Let's just go back. Alright. Slug. Okay. So now I've got the space. Right? Let's just log the params out here. Params. Why can't I like any ears? Params, slug, spaces announcements. Oh, it has to do with the URL structure now. Right? We've changed that. So we go back to our tag here. We don't really need this. Okay. So at least now we're getting an error. And what else do we have here? Index dot view slug space slug. Give it to me. Input string space dot cover image. Oh. What am I doing wrong here? So there's our announcements. Okay. It's because we don't have any. This would be v if space dot post dot length greater than 0. Solve the the issues. It doesn't like that. Vcard. Vfpost.coverimage.source.post.coverimagespace. Why does it unlike the unlike the discussion page? No worries, though. So we change the routing there. Alright. So now we are now we're getting somewhere. Right? We've got the routing the way that they do it. And then inside this post, we will actually fetch the individual post as well. Alright. So we'll come here. Let's just call this post so we can render that out. The data will be post. We are going to fetch the post dot dash slug. And instead of spaces, we're gonna do post. And in this case, this one is actually called post. Alright. What else do we need? It's gonna grab our comments as well. Alright. Boom. Okay. So now we can see our post. Looking great. If we go back, we'll grab the header image. K. Post dot cover image. Do we have a cover image for that post? Yes. We do. Why are we not sorting through that post dot cover image? Input must be a string. Oh, because we're fetching all of the data. Let's just delete that. That's probably why. Okay. So there's our cover image. Let's do a div. We're gonna do VHtml. This is gonna render out our actual content. So post dot contents. And Telwin has some really nice typography styles already preset for you, and we are going to just use those. Right? Pros. Missing our intag. Alright. Let's wrap this again. Div. We've got p x 4. Give us some padding. K. There's our post. Let's get into the comments. Right? How do we get down to the comments? We'll do just give this a little breathing room. P x a p y 8. 6 is not nearly enough padding. I definitely need to go in and, update the the actual mobile views for this as well. And then we have our comments section. How we doing on time? 55 seconds. Man, this one has been a little bit of a struggle. A little more to it than what I thought originally. Also operating on not enough coffee. So it's it's pretty painfully obvious at this point. Not going to get to any of the comments. So if we look at our functionality, right, we managed to get here. Users can log in. Did we even get there? We actually did, because it was already baked in, but we didn't we didn't really mess with that. Alright? There's a auth login page where I could actually log in to this thing. Users create posts and add comments to those posts, create and manage events. Man, like, we did not do so great on this one. So, sometimes you fail. Circle is a really cool app. You know, recreating that functionality, there's a lot baked into it. So, you know, hey. What do we give this one? Like, Cisco and Evert would probably give this one 2 thumbs down, but that's the way it rolls. You know, we've learned a lot about the different data models and how to use the different relationships inside Directus. So let's chalk it up as a a tie, maybe. I don't know. That's it for this episode of 100 apps, 100 hours. Thanks for joining me. We'll see you on the next one.",[311],"5d949bd4-c25c-47e7-a9f5-993c8d5c00a1",[],{"id":172,"number":131,"show":122,"year":173,"episodes":314},[175,176,177,178,179,180,181,182,183,184,185],{"id":182,"slug":316,"vimeo_id":317,"description":318,"tile":319,"length":320,"resources":8,"people":321,"episode_number":323,"published":324,"title":325,"video_transcript_html":326,"video_transcript_text":327,"content":8,"seo":328,"status":130,"episode_people":329,"recommendations":331,"season":332},"netflix","896165856","It's Bryant vs the streaming giant – Netflix – in this exciting episode. He has just one hour to build an app that replicates the core functionalities of the video streaming service.","012d9dd3-99b5-40ee-8811-9928bdc0a5bc",69,[322],{"name":199,"url":200},8,"2024-01-29","Mission: Netflix Clone","\u003Cp>Speaker 0: Alright. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus, and I'm super excited for today. We're gonna answer the question that has been keeping you up at night, can you build Netflix in an hour? You've probably not been keeping you up at night.\u003C/p>\u003Cp>But the truth? You can't handle the truth. I can't resist throwing in a good movie quote here for this one. So we are going to try to rebuild Netflix in 1 hour or less or publicly fail trying. Those are the rules of 100 apps, 100 hours.\u003C/p>\u003Cp>And then the second rule of Fight Club is use whatever you have at your disposal. So how are we going to achieve this? We are going to do a little planning, then we're going to do a little building. But just to get eyes on what we're going to build, here's Netflix. You know it.\u003C/p>\u003Cp>You've seen it. We are going to try to rebuild as much of this as possible in 1 hour. So that means the back end, that's where we'll start. Then we'll work on the front end and try to get as close as possible to the original Netflix or, my own spin on it as possible. So what do I have prepped ahead of time?\u003C/p>\u003Cp>I have a Nuxt application ready to go. I've also got an instance of Directus. Directus is what we're gonna be using for the back end. I've got it spun up. It is totally blank now and before we get started, I'll show you just so you know I'm not cheating.\u003C/p>\u003Cp>So we'll go to admin, example, we'll key in a quick little password. If I can actually remember what the password is. There we go. Okay. It is a totally blank instance, and if we fire up the database, again just to prove it, we've just got our directus collections that it adds for us automatically, so just the metadata.\u003C/p>\u003Cp>But as far as our actual data, there is not. So with that out of the way, I'm sure you guys trust me anyway, but let's get started. Right? We got 60 minutes on the clock. Boom.\u003C/p>\u003Cp>Away we go. Alright. So before we even get started, you know, maybe this is not the Netflix clone. This is, what are we going to call this, right? Give it a name.\u003C/p>\u003Cp>How about Dev Flix? Dev Flix, yeah, this is essentially what this could be. So this is Dev Flix, let's strike through this. This is what we're going to be building. And anytime I build a project, I I like to do some concepting first, maybe some diagramming, just so my brain is warmed up and I understand what's going on.\u003C/p>\u003Cp>So we'll just draw some boxes here. Let's walk through the functionality or do we even need to cover functionality, right? We all know what Netflix does, but we've got a, browse a catalog of titles. Those could be a movie or a show. What else?\u003C/p>\u003Cp>View that content. View stream videos. Right? Stream Videos once title is selected. Pick From different categories.\u003C/p>\u003Cp>Yeah, I don't know. This is Netflix, right? We all understand the functionality involved. Alright. So let's start working on the data model.\u003C/p>\u003Cp>Right? One of the ways that I will usually try to deconstruct application is just open up the JavaScript console and or the developer tools, and not necessarily the console. And then take a look at, like, the network requests that are coming in to see if I can understand what's going on behind the scenes. You know, I don't know that Netflix does Netflix have an API? Does it close its okay.\u003C/p>\u003Cp>So they closed their public API. But if we look, we could see there's a ton of calls going out to a player, but I see here's like some GraphQL stuff, some videos, artwork. I'm not really sure if this is gonna get me anywhere behind the scenes. They spend a lot of time on security and things like that and, you know, performance. So maybe they're doing some server side rendering and and things like that here to actually display this content.\u003C/p>\u003Cp>No worries. Right? But let's walk through and and just kind of think through things. Right? We've got what do we have inside Netflix?\u003C/p>\u003Cp>We have a, like, a title, like a content title, or, we could call it content, but that doesn't seem right. It could be a movie or a series. So let's just call it titles. Right? These are movies, series.\u003C/p>\u003Cp>There's a name for that, description, probably like a rating. You know, there's a lot of things. So we just wanna capture, like, the core stuff here. Alright. So we got titles.\u003C/p>\u003Cp>What else do we have as we go through? If we look at My Little Pony here on the screen, we've got our episodes and seasons. So if it's a show, we've got a season. Season has a number, probably, what, a release date for the season. And then it's gonna have a title ID and probably episodes.\u003C/p>\u003Cp>Right? Episodes. Now behind the scenes, I don't know exactly how Netflix Netflix has set up, like, their video and, you know, like, the the encoding and and streaming and all of that. I'm assuming attached to each title or each season, each episode. Alright.\u003C/p>\u003Cp>What do we have? Oh, sometimes you just accidentally push command q and totally destroy all of your progress. Alright. So how are we gonna structure this? Right?\u003C/p>\u003Cp>This is probably the most important thing to get right. Each episode is going to have a episode number, name, description. It's gonna be back to, what, a season, season ID. And then there's almost like another relationship here, which is just the actual content. So this is just as, like, the actual file.\u003C/p>\u003Cp>We'll probably just use, like, a YouTube URL in this case. Okay. So these are our kind of main collections. You probably also got things like ratings, users. Directus gives us that out of the box, which is really nice.\u003C/p>\u003Cp>So we've got Directus users. And, you know, we could have something like viewing history as well. User watch history. You know? We're getting really deep.\u003C/p>\u003Cp>Like, the main functionality is just up here where we have these items. Right? These are the core pieces of this application. Alright. Shoot.\u003C/p>\u003Cp>I forgot to start the clock. How long have we been doing this? Let's just give, like, 55 seconds or 55 minutes. Or I did start the clock, but we deleted Figma. Right?\u003C/p>\u003Cp>So let's handicap it a bit more. Maybe 54. Okay. Alright. So, we'll go back and look at that during editing, but, yeah.\u003C/p>\u003Cp>Sorry. Mistakes happen, especially when you're against the clock. Alright. So now that we've kind of got a rough idea of how we're gonna structure this, I like to work back end first because when I'm building my front end, I love having access to the actual data instead of lorem ipsum, dipsum, and then I have to wire everything up later. So with this stack, Directus and Nuxt, I can quickly scaffold my back end and get instant rest and GraphQL APIs, and then plumb that to my front end as I'm building the front end.\u003C/p>\u003Cp>So, it's a a really great workflow for me. I hope it works well for you. Give it a shot. Alright. So let's go in and create our first collection.\u003C/p>\u003Cp>We're gonna call this titles. And I'm sure somebody will tell me why this is wrong, why it is not. So Directus gives us a couple of system fields that we can add. These are just little helpers, like, date created is automatically populated with a time stamp. User created is automatically populated with a logged in user that created that.\u003C/p>\u003Cp>Do we have a sort for those? And I can already tell I forgot one big one, right, which is gonna be genres or categories. Like, we've gotta have some kind of taxonomy for those. Alright. So we've got our title.\u003C/p>\u003Cp>What is the type of this? Right? So it's either gonna be a movie or a series. Let's use our radio button interface for that. So this is gonna be the type.\u003C/p>\u003Cp>The first one will be movie. So it's either a movie. Let's say we default to movies. This is gonna be series. And and Directus also has built in translation strings, which is another nice thing that I could go in and create these translations where here's the key.\u003C/p>\u003Cp>Let's call it what? Series. Right? Series. And in English, this is gonna be let's just capitalize this field anytime we render it.\u003C/p>\u003Cp>Right? Cool. Alright. So then I could do something like this where I have dollar sign t and do series, and that will automatically translate that for me based on the strings that I've got set up, Which is really nice if you've got users across different languages inside your Directus instance inside the back end. Alright.\u003C/p>\u003Cp>So we got the type is either a movie or a series. Let's add an input for the name of this title. And I can even go in and add like a little helper. What's the name, in this notes section? What's the name of the content of the movie or series?\u003C/p>\u003Cp>Right? Or series. Great. What else do we have? We got a description.\u003C/p>\u003Cp>And let's just go for, like, a rating. So we got a drop down. Let's call it rating. Or, you know, we could probably have, like, a ratings collection here. I was probably thinking this is like reviews, but, let's just take a look and see.\u003C/p>\u003Cp>Right? This says this is TBY. Do we wanna be able to query by that? Right? So in that case, let's do a many to 1, and we'll call it a rating.\u003C/p>\u003Cp>And we're gonna have Directus create a new collection for us in the background. So everything that's going on here, I feel the need to explain this, Directus is actually mirroring this inside my database. So as I create these new fields, as they're called inside the Directus application, it is actually mirroring those to my SQL database. So we'll call this ratings. And if we go to advanced mode just to take a look at this, maybe we zoom back out a little bit.\u003C/p>\u003Cp>But I could see that Directus is telling me that, hey. We will create this inside your data model. Great. So, it's gonna create that for me. And then we've got a mini to 1 relationship.\u003C/p>\u003Cp>Great. Okay. So we got the rating. And now if I look, we've got our ratings collection created as well. Alright.\u003C/p>\u003Cp>So we'll go in, let's add a rating, the name for the rating. I'm not sure if that's what these are actually called or not. And let's just create a couple of these. Right? Like pg13, TV y.\u003C/p>\u003Cp>Is that what I saw? TV y. Yep. TV MA, rated r. Yep.\u003C/p>\u003Cp>Alright. So we get the the picture. We could call it PG, and then we have G. Right? Some of my favorite movies are still g rated movies, to this day.\u003C/p>\u003Cp>Of course, I'm a father of 3 as well. Alright. So we go back to our data model. Let's build out the rest of this. We've got our titles.\u003C/p>\u003Cp>So if we are creating a title now, like My Little Pony, now we can see what the status is. Is this a movie? Is it a series? What's the name, description? And we could pick a rating that we can then update through the system.\u003C/p>\u003Cp>And let's just adjust the display template for this because I wanna show the name anytime we're referencing that rating. Alright. So I'll zoom back in just a bit, and we'll build out our next piece of the puzzle here. Let's work on seasons. Alright.\u003C/p>\u003Cp>So we'll automatically generate a UUID. Maybe we wanna store who is the user that was updated this just so we can play the blame game later. Cool. Alright. And then we've got a number.\u003C/p>\u003Cp>So let's do an integer. We'll have the season number. Okay. And inside Directus, I can also make that the sort field. Where are you, sort field?\u003C/p>\u003Cp>Alright. So we'll choose the sort field. The season number is going to be the sort field. So whenever we reference this, it will sort by that number automatically. What else do we have?\u003C/p>\u003Cp>Do we have a description on the sort number? I I don't know. Release date, you know, this theoretically could be on the actual episodes as well. But Netflix does like to drop a season at a time, so maybe we store it here as well. Let's just use a time stamp so that it saves the the time zone value as well.\u003C/p>\u003Cp>Alright. What else? We have a title ID. Right? So we have to create that relationship back to the title so we can fetch that data.\u003C/p>\u003Cp>So this is gonna be a mini to 1. Right? A season can only belong, like, to a a single content title. So if if the show is My Little Pony, there's 5 seasons, all those seasons belong to 1 My Little Pony title. So we could call that title ID or title, I prefer less verbose, but it doesn't really matter here.\u003C/p>\u003Cp>Directus is going to serve up that however we create it. Alright. So we've got that. Let's add the one to many relationship. So we're creating a many to 1, but on the reverse side, a single title could have many seasons.\u003C/p>\u003Cp>So let's go ahead and create that relationship as well. And for the season we'll use the oh, for our many to 1 we'll use the name of that and then we'll do related values. Okay. So we got the title, we've got the number. You know, we could have a description for this season if we wanted to.\u003C/p>\u003Cp>You know, there's probably even, like, some collateral or trailers or or something attached to this. We won't worry about that right now. Let's save this, and then we'll go in and create episodes. So in our diagram over here, we probably should have had episodes to begin with. Alright.\u003C/p>\u003Cp>So same thing. I'll I'll just add my system field. So if anybody updates this information, we've got it. Is this episode published or not? Yes or no.\u003C/p>\u003Cp>And then we'll give this a name. What's the name of the episode? We probably also have a number for this episode as well. Episode number. And, you know, something like this, you may even call, like, episode number just to make sure everything is very clear, but I'm all for making things difficult.\u003C/p>\u003Cp>So we will add a description. And now we'll link this back to the season. Right? So we're going to use that many to one relationship again, and we're going to link this back to a season. The related collection is seasons.\u003C/p>\u003Cp>And I'm going to open up advanced field mode here in Directus, and I guess I could actually use my little mouse pose tool for you guys so we can get an idea of what's happening here. So I'm gonna add the reverse relationship back to seasons. So one season could have many episodes, 1 episode has or many episodes have a single season. Think I explained that right. Sounds great.\u003C/p>\u003Cp>Alright. So now we've got titles, we've got episodes, we've got seasons. If I look, we've got titles. So let's go in and create a new title. Right?\u003C/p>\u003Cp>So we'll just drag and drop Netflix over here. And let's just work on this. Right? My Little Pony, Make Your Mark. Alright.\u003C/p>\u003Cp>So this is My Little Pony, Make Your Mark. Alright. Maybe there's a hyphen there, a dot, I'm not sure. That's the season description. Right?\u003C/p>\u003Cp>Where do we see all episodes? Alright. So here's the there's kind of the the generic description that apparently Netflix does not let me copy. Alright. So welcome back to Equestria, where pony magic is everywhere.\u003C/p>\u003Cp>Who doesn't love pony magic? Alright. There's our description. This is TVY. Alright.\u003C/p>\u003Cp>And I can already see we've left out a couple things. Right? We've left out some thumbnail images, probably on each episode, each, title as well. But we'll go in and create a new season. They're calling this chapter 1.\u003C/p>\u003Cp>So we probably have a name for the season as well that we can add. Let's just say the release date is today, and we'll start adding some episodes. Right? So we got Make Your Mark. That's episode number 1.\u003C/p>\u003Cp>Something's wrong in Equestria. And now we're actually watching Netflix. How do I go back? Okay. Alright.\u003C/p>\u003Cp>Is there a way to actually I I guess they disable that. I'm just gonna use Raycast here to generate descriptions for this. Alright. So we'll say this is published. Alright.\u003C/p>\u003Cp>So at the very least, now we have some stuff that we can work with and fetch via the API that we can render on the front end. I'm gonna go back really quickly and just add a thumbnail for this title. And we could even do that for what, the episodes as well. We probably got, like, a thumbnail. There's probably also, like, a teaser.\u003C/p>\u003Cp>And while we're at it, let's just go ahead and try to finish out what we were building on the back end. Alright. So then we've got the actual content. So creating this separate collection allows me to standardize this across episode or, like, TV and movies. So, let's go in and create the content.\u003C/p>\u003Cp>You know, this could be video, I guess. Yeah. I'm not sure. Let's just call it content. And I'll probably figure out a better way to do this at some other point.\u003C/p>\u003Cp>Alright. So we got the content. We've got a just use a YouTube URL for this. We're gonna cheat a lot. But this could be like an actual file that we uploaded to our back end here that we serve.\u003C/p>\u003Cp>Again, I don't want to get into, like, the streaming stuff because I like efficiently serving video at different bandwidth, at different resolution. That's a whole ball of wax. I'm sure there's a great API for it. And Directus will manage some of those assets for you in a nice way, but it doesn't do video encoding and things like that that you need to deliver those streams effectively. Cool.\u003C/p>\u003Cp>So we've got, what, a YouTube URL. There's the content. This is gonna have a title associated with it. Titles. K.\u003C/p>\u003Cp>And then there's probably what? Episode as well? Episode. Titles. Okay.\u003C/p>\u003Cp>Alright. So we got a title. We got an episode. We probably should have created those reverse relationships, but we could go in and do that now. Alright.\u003C/p>\u003Cp>So we'll call this the content. That'll be our content collection. Episodes. And now we'll just create the reverse. Uh-oh.\u003C/p>\u003Cp>What am I doing wrong? I'm doing a mini to 1 back to that when, actually, let's just go in and I'm gonna delete these for now just to make sure we set these up correctly. Alright. So I'm gonna go let's go to the titles. We'll do a many to 1.\u003C/p>\u003Cp>We'll call this content. We'll go to the content collection. And here, I am going to go into the relationship. We'll add this to the title. Okay.\u003C/p>\u003Cp>So we'll add that reverse one to many relationship. We'll call it the title. I think it is going to be an array. I guess you could have multiple pieces of content attached to any one item, but no worries. Content.\u003C/p>\u003Cp>And then we're gonna have episode. Okay. Okay. Cool. Cool.\u003C/p>\u003Cp>Cool. Alright. What's next? Right? We probably need some genres.\u003C/p>\u003Cp>Let's go in genres. And if there could be multiple genres per piece of content, I assume, like, could be drama and horror or drama and faux drama. If you live in my household, I've got plenty of drama. So we'll give this a name. And then let's go in, and we would create a junction table between titles and genres.\u003C/p>\u003Cp>Right? But, again, one of the nice things here is Directus is gonna do a lot of this for us. So we could call this genres. We do genres as the related collection. And if we look, we go to the advanced mode inside Directus, we can see that there's gonna be a junction collection or a junction table in our database created.\u003C/p>\u003Cp>Do we want to be able to query by genres? Maybe we don't need that. Do we add a sort field? Probably not. Okay.\u003C/p>\u003Cp>So now we've got genres. We've got thumbnail. We got a lot of different items set up in this database. Right? And we're closing in on 30 minutes.\u003C/p>\u003Cp>We probably need to start building some UI soon. But I do wanna just open up the database itself and and take a look at what's going on now. So you'll see these direct us tables. But if you look, you can also see our our actual tables that we created, like episodes and genres. And if I look at the data, here's the episode that we created, make your mark for My Little Pony.\u003C/p>\u003Cp>So this is great. Like, anything that happens inside the app here, Directus is mirroring that to your database. Likewise, if I go in and, there's very robust access control here. I'm just gonna enable this as a public permission for all of our content, which, would probably not be a great thing security wise, but I just want to show you this. We'll go into local host 8055, which is my back end.\u003C/p>\u003Cp>And if I go to items and I go to titles, boom, I'm gonna get My Little Pony. Right? Now, this is not super helpful because we're just getting you know, like, let's say I I just wanted to make a single call and get all my content. I love Directus for that because it gives me an interface via REST that feels like GraphQL. So I get the benefits of GraphQL and then I can go in and do something like this, like, hey, I just want the ID, I want the type, I want the name, I want the description, Boom.\u003C/p>\u003Cp>And it will give me exactly what I asked for. Now, the other thing that I can also do is go in and if I want something like seasons, and I I could use a wildcard to grab all the root level fields of the the next one, right, but I can get all of my related content in a single API call. That's super powerful, super helpful. Obviously, like, the deeper you go, the slower the queries on the back end are gonna be, but it is still very speedy. And, obviously, I don't have a ton of data here.\u003C/p>\u003Cp>So we got seasons dot star, and then we could do seasons dot episodes dot star. And that should give us the episodes. And then once we add content, we could go even deeper and say something like seasons dot episodes dot, content dot star. Now there is no content at this point, but if there were, it would come back. Great.\u003C/p>\u003Cp>So that is just one way that one of the major things that I really love about directives. Alright. So we've got some stuff going on. Let's go in and populate a few more titles. Right?\u003C/p>\u003Cp>We've got a thumbnail. I don't think they're gonna let me lift this. So let's just do the old screenshot trick. Upload that image. Click to browse.\u003C/p>\u003Cp>Should actually just be able to drag and drop there. Okay. Let's create a new content for this. We'll call it YouTube. Pull you up, My Little Pony.\u003C/p>\u003Cp>And again, I I don't support any of this. Make sure anything that you are doing is okay legally. We'll just copy this. K. Great.\u003C/p>\u003Cp>We've got a movie. Actually, this is a series. Right? But one thing I noticed here is if I'm switching between movies and series, we should probably actually not show our seasons. Right?\u003C/p>\u003Cp>So how do we actually adjust that? Well, if we go into our titles, Directus has the ability to do, relational or, I'm sorry, conditional fields. So I could go in and for seasons, we could do something like this where we hide this by default, and then we come into our conditions tab. And if type equals series, so then we build a rule. This is just a description for this.\u003C/p>\u003Cp>We'll do type type equals Series, then we are going to make this not hidden. Right? And we'll show a link to the item. Did I get that right? Yep.\u003C/p>\u003Cp>Okay. Alright. So let's test this out just to see what's up. Let's go in and add another one. Right?\u003C/p>\u003Cp>And now where I have movie selected, nothing. But if I select series, now we're seeing that field. It doesn't change the underlying database structure or anything. It's just hiding that when I'm creating the content. But I could even add validation around it if I wanted to.\u003C/p>\u003Cp>So if we go in and we add a movie, let's see if we've got a movie. I could switch up here to the top to movies, and we open this up. We've got a trash truck Christmas. Christmas. Alright.\u003C/p>\u003Cp>This is TV Y, made for TV, made for TV goodness. Alright. We will create some new content for this. Where's our handy YouTube? Trash truck Christmas, full episode Netflix Junior.\u003C/p>\u003Cp>This looks like an official channel. Yeah. Okay. We'll see. So we got that.\u003C/p>\u003Cp>We've got a thumbnail image. Again, I'm not even gonna try to lift this from oh, actually, I I guess I could. Copy image address. Is that available? On the web?\u003C/p>\u003Cp>Okay. Cool. So we'll just lift these images directly. I can import that file. Great.\u003C/p>\u003Cp>There that is. And I could, you know, maybe potentially find their GraphQL, stream for this that allows me to pull all this in, in a a more timely fashion. But, okay. Genres. Right?\u003C/p>\u003Cp>Let's create a new genre. What are we calling this? This is kids TV. There's a new genre. Okay.\u003C/p>\u003Cp>We'll create a new kids and family movies. And this will be TV cartoons. Great. Okay. So we got a movie.\u003C/p>\u003Cp>Let's switch this back to a series. Right? Lots of series because we've got seasons attached to it. Got some content for it. Is it is that gonna be right?\u003C/p>\u003Cp>Actually, it's not, is it? We're going to go back to My Little Pony. We'll go into the seasons. We've got episodes, and we should have content for that episode. Cool.\u003C/p>\u003Cp>Alright. So let's just see what this all looks like. Great. I could even go in and adjust this to, like, something akin to Netflix if I wanted to where I could see, like, the thumbnails of all these different titles. So give it a name.\u003C/p>\u003Cp>The subtitles could be what? Description. What else do we have? Or it could also be can we get genres in here? Genre genres ID.\u003C/p>\u003Cp>Let me get the name of the genres. Cool. Alright. So we got a couple titles. We are what?\u003C/p>\u003Cp>We're pretty I'm pretty happy with where the back end is at at this point. You know, we could really go through this in a lot more detail and create something like actors, you know, the cast and crew, director, things like that. And those would probably all be different database tables. But for the sake of having something to display at the end of this next 28 minutes, let's just get to work on the front end. Alright?\u003C/p>\u003Cp>Cool. So we will let's put you back over here, Netflix. Close you. We'll close our database. Let's pull up our Nuxt application, and I'm just gonna swap these around.\u003C/p>\u003Cp>No real rhyme or reason. I just like my text editor being on the right side. Alright. So now let's pull up our Nuxt application. Oh, this thing is a beauty, right?\u003C/p>\u003Cp>So, I've just got a preconfigured, like, a Directus Nuxt module set up that basically just wraps the Directus SDK with a little bit of auth. You know, we may not even dive into that. But, and then I've got a use direct as composable that allows me to make requests easier. There is a a little bit of setup work that I've done here, but it is really just, one of our boiler plates HSC OS that I have taken and stripped back for the purposes here. Alright.\u003C/p>\u003Cp>So we go into our index page. Alright. So we let's look at, the homepage here of Netflix. And we've got kind of this setup of a header. We've got a grid of titles sorted by genre.\u003C/p>\u003Cp>Alright. So let's dive into this. The first thing that I'm going to do, just because we are short on time, is take a look at, Tailwind UI. Advanced. What's going on here?\u003C/p>\u003Cp>Okay. Let's just look at Nux UI as our our library here. Something wrong with my laptop at this point. Alright. So inside this boilerplate that I've got set up, I've got a little, I've got I've got this Nuxt UI library, which is kind of built on Tailwind as well, that gives us some nice things like modals, some of the trickier components to actually build.\u003C/p>\u003Cp>Right? So we go in. Let's try Tailwind UI again, now that I've refreshed a little bit. And, alright, so what are we looking for? Let's grab a, like, a header from Tailwind UI.\u003C/p>\u003Cp>Alright. This looks pretty good here. So we've got the view version. I'm just going to copy this down. Alright.\u003C/p>\u003Cp>And let's go in and create a header, error component header dot view. Alright. Are we gonna have a logo for this, right? We've got our logo. Can we pull that up?\u003C/p>\u003Cp>Will this let me copy as PNG? Can I export this as JPEG? Nope. Let's just do a new Figma file actually instead of FigJam. Can we paste that?\u003C/p>\u003Cp>If we copy, Can we paste? Devflix. Yeah. Alright. Let's give it some, like, type of little icon treatment here.\u003C/p>\u003Cp>So we'll add a real original logo here. Looks like a play button. We'll change the fill to red. Right? Okay.\u003C/p>\u003Cp>And maybe we, what, let's wrap this in. Let's give it a little oh, yeah. I'm I'm gonna be giving design courses after this as well. Alright. So we'll take this.\u003C/p>\u003Cp>We'll export to SVG. Let's just do logo. Great. Alright. So we've got this header component.\u003C/p>\u003Cp>If we go back to Netflix, what do we got at the top? We've got, home. That'll be our index page. This is for our navigation. Right?\u003C/p>\u003Cp>So we've got TV shows. Alright. We got movies. Great. Okay.\u003C/p>\u003Cp>Cool. I don't have Headless. I don't have heroicons in here, so let's just comment those out for now. Bars 3. I'm sure that'll throw a fit when we actually take a look at this.\u003C/p>\u003Cp>But cool. Alright. Now I'm gonna create a new file called logo dot view. And we're just gonna copy this as, what, SVG. Paste here.\u003C/p>\u003Cp>Nope. I need to wrap that in a template, though. Alright. There's our SVG. Now inside our header component, I'll go back up here to where we have Tailwind UI's logo.\u003C/p>\u003Cp>And I'm just going to call that logo component that we just had. Don't need a source. Don't need an alt tag. Well, we probably do, but not going to have one. 100 apps, 100 hours.\u003C/p>\u003Cp>Okay. So we have a header. We're not seeing it here. What I can do is adjust the default layout. And let's do something like this where we have BG gray 900 and probably text white since we're kinda going on the Netflix theme.\u003C/p>\u003Cp>So this is the layout inside Nux, which is a feature that I really like about Nux, is the ability to set up a default layout. And boom, we've got our DevFlix header looking nice. If we go back to that header, probably wanna swap these a tags or a Nuxt link and change this to 2. And we could change this to TV shows and movies. Right?\u003C/p>\u003Cp>We just have to create a route for those. And I'm not sure exactly how far we'll get because we have, what, 21 minutes remaining. But we'll we'll see how far we can get. Alright. So if I go into the header, we need to update the details for it as well, like bg white.\u003C/p>\u003Cp>What do we what does Netflix look like on the header? It's just kinda transparent until you scroll down. That's a nice effect. I don't want to set up anything there. So let's just cheat.\u003C/p>\u003Cp>We'll call it bggray900 again. And then that's gonna mess up a lot of our text, I believe. Right now, we can no longer see all of that. So we go to our text. Let's look at anything with text gray.\u003C/p>\u003Cp>Can we replace that with we could probably do smaller values, like text 200. Again, I love Tailwind just because it enables me to not have to write custom CSS for everything. Like, it it does take some time to learn the conventions, but but being able to not write CSS, I like custom CSS, just helps me speed along. Now I know it's not for everyone, so don't let me tell you that it is, But I enjoy it. Alright.\u003C/p>\u003Cp>Can we get our stuff? Okay. Alright. That's probably because we're missing an icon for our navigation over here. So what is this?\u003C/p>\u003Cp>This is hidden. Flex LG. Let's hide that at, like, just a smaller breakpoint. Yeah. Okay.\u003C/p>\u003Cp>Cool. Alright. So we got home. We got TV shows. We got movies.\u003C/p>\u003Cp>We can log in. Great. Now let's actually work on our page. Right? If we go in, let's actually render some stuff out.\u003C/p>\u003Cp>Alright. So if I look at Netflix, we've got a, like, a header component or, like, a hero image. So we go for our hero sections. Hero. Which one of these kinda looks like Netflix?\u003C/p>\u003Cp>Kind of this one? Kind of. I mean, this one has a background image. So we could take that. Okay.\u003C/p>\u003Cp>So we're not gonna copy this header piece, but let's just go in. I'm gonna copy this part. We will create a new one called hero dot view. I've got a just a couple of snippets already set up for me inside Versus Code to make creating Vue components a little easier. And k.\u003C/p>\u003Cp>We're good there. Watch chapter 6 now. Is this just kind of a rotating thing, probably based on, like, the last thing that you watched? Instead of doing that, let's do something else. Data.\u003C/p>\u003Cp>Alright. Alright. So this is gonna be justified center. We don't wanna do that. We wanna do does it justify start, I believe?\u003C/p>\u003Cp>K. Oh, we'll add this to our index page. So let's take a look at this. We'll just wrap this in a div. We'll go to hero, call in that component.\u003C/p>\u003Cp>Let's see what this is starting to look like. Alright. Okay. Okay. We got a little something going on here.\u003C/p>\u003Cp>I only succeeded in moving one thing to the left. So let's take a look at that hero component one more time. Alright. So this has got some type of gradient blur attached to it for the images. Can we lift this?\u003C/p>\u003Cp>Oh, yes, we can. Netflix. Love building on the fly. Thank you. Netflix.\u003C/p>\u003Cp>What else do we have? The overflow hidden. The the the announcing. Do we even need this? Let's let's just scrap that part of it.\u003C/p>\u003Cp>And we'll just show watch chapter 6 now. And I still can't copy this stuff. Come on, Netflix. Copy. Copy selector, copy element, edit as HTML.\u003C/p>\u003Cp>All right. So we'll just pick this up and throw it here. And we got the My Little Pony logo, so we'll add in Nuxt. We'll just add in an image tag because I don't have that domain configured. Image source.\u003C/p>\u003Cp>Alright. Class. I don't know. Maybe height 24. And an image tag, welcome back to Equestria Tech Center.\u003C/p>\u003Cp>This is not gonna be Tech Center. Okay. We're getting somewhere. How do we dim this image? Right?\u003C/p>\u003Cp>Brightness 50? Oh, wrong image, Brent. Brightness, 50. Maybe 25. Oh.\u003C/p>\u003Cp>Can we do a percentage here? Get, like, a a nice value. Okay. Alright. So we're getting closer.\u003C/p>\u003Cp>He likes it. Alright. Get started. Let's say watch now. Okay.\u003C/p>\u003Cp>Let's remove that one. Alright. So at least we got some type of header here. This would be a NuxLink. And, I'm trying to think of how we're gonna serve this content.\u003C/p>\u003Cp>Right? We would do a page. Let's add a new folder for this. This is gonna be what does Netflix do as far as routing? Right?\u003C/p>\u003Cp>If I open this up, episodes and info. Okay. So is it just like a genre? Is that how how the URL structure is? Because you watch Masha's spooky stories.\u003C/p>\u003Cp>So we got the genre. If I look at the URL again, yeah, it's just like Netlify Kids, and then there's a query param. Alright. So it actually appears to be inside the the actual application. Right?\u003C/p>\u003Cp>So we're just appending, like, a query per annum to it. Let's come back to that step. Let's actually render out some titles first, though. Right? So we go back to our page.\u003C/p>\u003Cp>Let's create a new component. What what do we gotta call this? Content grid? Sounds great. Vcomp ts.\u003C/p>\u003Cp>Okay. So now we've got a a Vue component. Let's do some data fetching. Right? We will do something like this where, we got constant data equals use async data.\u003C/p>\u003Cp>So this is the built in Nuxt data fetching. We give it a key. Let's call it content grid, comma. Alright. And then we're going to return something.\u003C/p>\u003Cp>So in this case, we're gonna use our directus composable that I've got inside this. But if not, I could import the Directus SDK, fire that up, but, we're on the on the clock here. So alright. So we're gonna read our items, and we are going to read titles. So and then we're gonna get, is there any specific thing that we wanna render out?\u003C/p>\u003Cp>Let's just leave our options blank for now. Right? So we got our data. Let's just go in and make sure we're getting that data back. Sounds great.\u003C/p>\u003Cp>I don't see it on the screen. Why not? That's because we're not actually including it in our index page. So we'll just do content grid. Great.\u003C/p>\u003Cp>Okay. So at least we could see a few things here. That's great. Do we really need anything else to render this grid? Probably not, actually.\u003C/p>\u003Cp>This shows, like, the viewing data. We're we're definitely not gonna get to that. We got 12 minutes left. Let's just see if we can actually render something out. It shows something in a modal.\u003C/p>\u003Cp>All right. So we got our content grid. Great. Cool. Let's go in.\u003C/p>\u003Cp>I'm not sure if Tailwind has a great looking component for this. Gridless. Kind of. Yeah. Let's just create something on the fly.\u003C/p>\u003Cp>Right? So we'll go in. Alright. So that's the wrapper div. Then we'll do, we would probably fetch this via genre as well.\u003C/p>\u003Cp>Again, running short on time. Alright. We are going to do a, let's just keep this simple. Right? Add add a little bit of padding to the sides.\u003C/p>\u003Cp>We'll give this a grid. Grid calls, 5 across the screen, 6 across the screen. And then we'll do, let's do a, what, title card? And we will pass so we'll do some v four action here. V 4 title and data.\u003C/p>\u003Cp>The key is gonna be title dot ID. And then we have our, what, We're going to pass a title, title equals title. All right. So now we're going to create a new component. Call it title card dot view.\u003C/p>\u003Cp>And we will define some props. So props equals define props, Title object is prop type. Haven't set up our types here even though we're still using TypeScript. Alright, so we've got a card. I know Telen has like a card definition.\u003C/p>\u003Cp>Actually, the Nuxt UI library has a card as well. Let's just use it. So we got a card. There's a card. U card is the name of this.\u003C/p>\u003Cp>U card. And what do we have inside the card? We have, I don't know if these actually need to be h tags or not. We'll just use a p tag. Got the title dot name.\u003C/p>\u003Cp>If I can actually type this. Right? We've got an image for the card. Source equals title. No, no, no, no.\u003C/p>\u003Cp>We're gonna use Nuxt image because I have it preconfigured. And I'll just show you that really quickly. If I go to Nuxt Config, ba ba ba ba, where are you? Where are you, mister Image? Image.\u003C/p>\u003Cp>Okay. So Nuxt Image Directus is a provider here. We just give it a base URL that communicates with our Directus instance and allows us to do image transformations on the fly, which is really nice. So we got source title dot thumbnail. K.\u003C/p>\u003Cp>What do we do? Like, what? With full height 24 maybe? Sounds good. We got a title.\u003C/p>\u003Cp>Let's make this font bold. I'm assuming that's what it looks like. Oh, no. It's actually just a card. Right?\u003C/p>\u003Cp>Alright. Let's see what this looks like. We'll go back to our index page, content grid. Why don't we have our content? Why is content grid not rendering anything?\u003C/p>\u003Cp>Do we save it? What's going on here? Dev server. Why are we not rendering? Title card.\u003C/p>\u003Cp>Content grid. There is no data. What are we doing there? We just had our data. Right?\u003C/p>\u003Cp>Am I being silly? Pre data. Is that not showing? Did I break something? What is going on?\u003C/p>\u003Cp>Use, async data, content grid, titles, read items. Where did you go? Did I not just have this, like, a minute ago? Title. Title equals title.\u003C/p>\u003Cp>Hydration mismatch. If I look at content grid, like, we should have data. Right? Let me just look at the Nuxt fetching. Sometimes I even I use Nuxt all the time, and I'm not sometimes I have to go back to the oh, we gotta wait.\u003C/p>\u003Cp>Yeah. I don't know how that worked the first time. Right? Next image not showing. Okay.\u003C/p>\u003Cp>So we're missing some images there. Why are we missing images? So we are missing images because, let's do let's add our grid back. Grid calls 6bg grid. Do we need an actual BG?\u003C/p>\u003Cp>Okay. Maybe grid calls 4. That's fine. How are we doing on the time? We got 6 minutes left to actually render something up.\u003C/p>\u003Cp>Px8py12. Just pad this out a little bit. Alright. What's next? Our our images are not displaying.\u003C/p>\u003Cp>Right? How do we fix that? It is actually gonna be inside our Directus instance. So it is a permissions issue. Pretty sure if I were to take a look at our network requests, look at it like images here, you have 403 forbidden.\u003C/p>\u003Cp>So, I would go into not my docs. We'll open up the Directus instance. Go to 8055. And I somehow managed to log myself out as well. Alright.\u003C/p>\u003Cp>We go into access control. We've got our content here, but then we're gonna go in and allow access to the direct us files collection. So that way we can actually serve those images on the front end. Okay. Great.\u003C/p>\u003Cp>Maybe title card wasn't a great idea there by using the Nux card. Right? Let's just call it div. Oh, forgot to close that. Alright.\u003C/p>\u003Cp>So there's the diff. This is going to be Object, Cover, Trash Truck Christmas. Maybe we need to make that a little higher, 32. Alright. How we doing on time?\u003C/p>\u003Cp>4 minutes. Can we get this to actually render? Alright. Button at click. Alright.\u003C/p>\u003Cp>Quick and dirty. Let's do this. We're going to add a composables. Actually, we could just pass this up as well. Alright.\u003C/p>\u003Cp>At click, we're going to define some omits. So omits equals define omits. We're gonna tell this select title. Select title? I think that's the syntax.\u003C/p>\u003Cp>Alright. Alright. Select title. Emit. Select title.\u003C/p>\u003Cp>And we're gonna pass the what? Prop no. It should just be title dot ID. Is that right? Or does that do I have to wrap that?\u003C/p>\u003Cp>No. Where are we getting here? Select title does not exist. Let's just call this function. On click, emit select title, props dot title, on click.\u003C/p>\u003Cp>Cool. Okay. And then inside our content grid, let's do this where we've got a modal. K. And we've got is open.\u003C/p>\u003Cp>That's false. Select title. And then what are we gonna do for the title? Let's do title, maybe content. Right?\u003C/p>\u003Cp>Content ref equals null. GitHub Copilot here for the win on this one. Right? Do we have a video component? I don't have a video component.\u003C/p>\u003Cp>So we're just gonna do, what, an iframe, class, aspect, style. Let's call it video. We'll just add a quick style here. Dot video aspect ratio 16 by 9. Alright.\u003C/p>\u003Cp>The well, the source is gonna be the content dot YouTube URL. Is that actually gonna work? I think I've got do I have some strings in here? Do I have a video content? I don't.\u003C/p>\u003Cp>Alright. Let's rely on AI here. Generate YouTube embed. Okay. So that returns the video ID.\u003C/p>\u003Cp>Let's see what AI comes back with. YouTube embed video ID. Okay. Great. Alright.\u003C/p>\u003Cp>So there's gonna be our source. We'll say generate YouTube video embed URL. Oh, nope. That's YouTube embed URL. Like, somewhere in the starter, I may already have this.\u003C/p>\u003Cp>Generate YouTube embed URL, and that is gonna be the content dot YouTube URL. Alright. So what am I missing? I'm gonna need to close that. Okay.\u003C/p>\u003Cp>And then on our select title, right, we need to fetch the content for the title. Alright. So that's gonna be what? This is gonna be an async function now. How are we doing on time?\u003C/p>\u003Cp>We have hit time. So we ran out of time to actually flesh this out and render this content in a model, but I just wanna try it. Let's just pursue it really quickly and see where we're at. Constant. We are going to we don't need to actually use async data for this.\u003C/p>\u003Cp>We're gonna fetch this on the client side. So let's do constant title equals wait. Use direct us, read items, title. Read item. Is it gonna be a title?\u003C/p>\u003Cp>We're gonna fetch through the we got the root level fields. Let's take a look at that data model again. Right? Local hosts. Alright.\u003C/p>\u003Cp>So we got titles, then we've got content? Alright. So root level, we'll go content dot that. Select title. Title has already been declared.\u003C/p>\u003Cp>Oh, okay. Where do I have that at? Oh, I've already given it the title. Content. Alright.\u003C/p>\u003Cp>So if I click that, is it actually making the request? Select title. Oh, and then for the title card at select title, we're going to select title. Alright. So I can see that's making the request.\u003C/p>\u003Cp>I get the data here. There's our content. Do we have the YouTube URL? That's great. For some reason, it's not throwing the modal window.\u003C/p>\u003Cp>Right? Why is the modal not opening? Cannot read properties, undefined of YouTube URL. Alright. So we probably wrap this in, like, a computer or something.\u003C/p>\u003Cp>Selected content equals computed value. Alright. And this is going to be if selected content and we'll have selected content. Is that why? Alright.\u003C/p>\u003Cp>So it's actually throwing the modal, but why isn't that working? It's because I'm not passing the right stuff. Right? So it should be dot content dot YouTube URL. Is that gonna work?\u003C/p>\u003Cp>Where are you? Where are you? Modal. Title card. Where's the content grid?\u003C/p>\u003Cp>Our selected content. Content dot value. Oh, duh. Gosh. Forgetting to set the content.\u003C/p>\u003Cp>Content dot value equals content. Let's try it now. No. Still new. Okay.\u003C/p>\u003Cp>So I hate to waste everybody's time. Content dot oh, fetched content. Let's just try this real quick before I give this up. Equals fetched content. Holy moly.\u003C/p>\u003Cp>And there it is. Right? We have a Netflix clone. So a little after time, we made it actually render something, which is a win. Kudos to the Netflix team.\u003C/p>\u003Cp>Right? The truth of the matter is that it is very difficult to build Netflix in an hour. So I hope you enjoyed this episode. Like, what would happen next for me? Basically, continue to flesh this out, get the UI working and and wired up correctly.\u003C/p>\u003Cp>You know, if I wanted to copy Netlify or Netflix directly, I would probably make sure that when I loaded a title that was getting appended to the the actual query string in the URL. You might set it up on a different route that renders a model. But next steps would just be basically continuing down the route of fleshing out the the UI side of it and, you know, adjusting the data model inside our back end as needed. So that's it for this episode of 100 apps, 100 hours. The outcome, maybe we give it one thumbs up.\u003C/p>\u003Cp>You know, I'm sure Cisco and Evert would probably not approve. So I'll catch you on the next one. See you.\u003C/p>","Alright. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus, and I'm super excited for today. We're gonna answer the question that has been keeping you up at night, can you build Netflix in an hour? You've probably not been keeping you up at night. But the truth? You can't handle the truth. I can't resist throwing in a good movie quote here for this one. So we are going to try to rebuild Netflix in 1 hour or less or publicly fail trying. Those are the rules of 100 apps, 100 hours. And then the second rule of Fight Club is use whatever you have at your disposal. So how are we going to achieve this? We are going to do a little planning, then we're going to do a little building. But just to get eyes on what we're going to build, here's Netflix. You know it. You've seen it. We are going to try to rebuild as much of this as possible in 1 hour. So that means the back end, that's where we'll start. Then we'll work on the front end and try to get as close as possible to the original Netflix or, my own spin on it as possible. So what do I have prepped ahead of time? I have a Nuxt application ready to go. I've also got an instance of Directus. Directus is what we're gonna be using for the back end. I've got it spun up. It is totally blank now and before we get started, I'll show you just so you know I'm not cheating. So we'll go to admin, example, we'll key in a quick little password. If I can actually remember what the password is. There we go. Okay. It is a totally blank instance, and if we fire up the database, again just to prove it, we've just got our directus collections that it adds for us automatically, so just the metadata. But as far as our actual data, there is not. So with that out of the way, I'm sure you guys trust me anyway, but let's get started. Right? We got 60 minutes on the clock. Boom. Away we go. Alright. So before we even get started, you know, maybe this is not the Netflix clone. This is, what are we going to call this, right? Give it a name. How about Dev Flix? Dev Flix, yeah, this is essentially what this could be. So this is Dev Flix, let's strike through this. This is what we're going to be building. And anytime I build a project, I I like to do some concepting first, maybe some diagramming, just so my brain is warmed up and I understand what's going on. So we'll just draw some boxes here. Let's walk through the functionality or do we even need to cover functionality, right? We all know what Netflix does, but we've got a, browse a catalog of titles. Those could be a movie or a show. What else? View that content. View stream videos. Right? Stream Videos once title is selected. Pick From different categories. Yeah, I don't know. This is Netflix, right? We all understand the functionality involved. Alright. So let's start working on the data model. Right? One of the ways that I will usually try to deconstruct application is just open up the JavaScript console and or the developer tools, and not necessarily the console. And then take a look at, like, the network requests that are coming in to see if I can understand what's going on behind the scenes. You know, I don't know that Netflix does Netflix have an API? Does it close its okay. So they closed their public API. But if we look, we could see there's a ton of calls going out to a player, but I see here's like some GraphQL stuff, some videos, artwork. I'm not really sure if this is gonna get me anywhere behind the scenes. They spend a lot of time on security and things like that and, you know, performance. So maybe they're doing some server side rendering and and things like that here to actually display this content. No worries. Right? But let's walk through and and just kind of think through things. Right? We've got what do we have inside Netflix? We have a, like, a title, like a content title, or, we could call it content, but that doesn't seem right. It could be a movie or a series. So let's just call it titles. Right? These are movies, series. There's a name for that, description, probably like a rating. You know, there's a lot of things. So we just wanna capture, like, the core stuff here. Alright. So we got titles. What else do we have as we go through? If we look at My Little Pony here on the screen, we've got our episodes and seasons. So if it's a show, we've got a season. Season has a number, probably, what, a release date for the season. And then it's gonna have a title ID and probably episodes. Right? Episodes. Now behind the scenes, I don't know exactly how Netflix Netflix has set up, like, their video and, you know, like, the the encoding and and streaming and all of that. I'm assuming attached to each title or each season, each episode. Alright. What do we have? Oh, sometimes you just accidentally push command q and totally destroy all of your progress. Alright. So how are we gonna structure this? Right? This is probably the most important thing to get right. Each episode is going to have a episode number, name, description. It's gonna be back to, what, a season, season ID. And then there's almost like another relationship here, which is just the actual content. So this is just as, like, the actual file. We'll probably just use, like, a YouTube URL in this case. Okay. So these are our kind of main collections. You probably also got things like ratings, users. Directus gives us that out of the box, which is really nice. So we've got Directus users. And, you know, we could have something like viewing history as well. User watch history. You know? We're getting really deep. Like, the main functionality is just up here where we have these items. Right? These are the core pieces of this application. Alright. Shoot. I forgot to start the clock. How long have we been doing this? Let's just give, like, 55 seconds or 55 minutes. Or I did start the clock, but we deleted Figma. Right? So let's handicap it a bit more. Maybe 54. Okay. Alright. So, we'll go back and look at that during editing, but, yeah. Sorry. Mistakes happen, especially when you're against the clock. Alright. So now that we've kind of got a rough idea of how we're gonna structure this, I like to work back end first because when I'm building my front end, I love having access to the actual data instead of lorem ipsum, dipsum, and then I have to wire everything up later. So with this stack, Directus and Nuxt, I can quickly scaffold my back end and get instant rest and GraphQL APIs, and then plumb that to my front end as I'm building the front end. So, it's a a really great workflow for me. I hope it works well for you. Give it a shot. Alright. So let's go in and create our first collection. We're gonna call this titles. And I'm sure somebody will tell me why this is wrong, why it is not. So Directus gives us a couple of system fields that we can add. These are just little helpers, like, date created is automatically populated with a time stamp. User created is automatically populated with a logged in user that created that. Do we have a sort for those? And I can already tell I forgot one big one, right, which is gonna be genres or categories. Like, we've gotta have some kind of taxonomy for those. Alright. So we've got our title. What is the type of this? Right? So it's either gonna be a movie or a series. Let's use our radio button interface for that. So this is gonna be the type. The first one will be movie. So it's either a movie. Let's say we default to movies. This is gonna be series. And and Directus also has built in translation strings, which is another nice thing that I could go in and create these translations where here's the key. Let's call it what? Series. Right? Series. And in English, this is gonna be let's just capitalize this field anytime we render it. Right? Cool. Alright. So then I could do something like this where I have dollar sign t and do series, and that will automatically translate that for me based on the strings that I've got set up, Which is really nice if you've got users across different languages inside your Directus instance inside the back end. Alright. So we got the type is either a movie or a series. Let's add an input for the name of this title. And I can even go in and add like a little helper. What's the name, in this notes section? What's the name of the content of the movie or series? Right? Or series. Great. What else do we have? We got a description. And let's just go for, like, a rating. So we got a drop down. Let's call it rating. Or, you know, we could probably have, like, a ratings collection here. I was probably thinking this is like reviews, but, let's just take a look and see. Right? This says this is TBY. Do we wanna be able to query by that? Right? So in that case, let's do a many to 1, and we'll call it a rating. And we're gonna have Directus create a new collection for us in the background. So everything that's going on here, I feel the need to explain this, Directus is actually mirroring this inside my database. So as I create these new fields, as they're called inside the Directus application, it is actually mirroring those to my SQL database. So we'll call this ratings. And if we go to advanced mode just to take a look at this, maybe we zoom back out a little bit. But I could see that Directus is telling me that, hey. We will create this inside your data model. Great. So, it's gonna create that for me. And then we've got a mini to 1 relationship. Great. Okay. So we got the rating. And now if I look, we've got our ratings collection created as well. Alright. So we'll go in, let's add a rating, the name for the rating. I'm not sure if that's what these are actually called or not. And let's just create a couple of these. Right? Like pg13, TV y. Is that what I saw? TV y. Yep. TV MA, rated r. Yep. Alright. So we get the the picture. We could call it PG, and then we have G. Right? Some of my favorite movies are still g rated movies, to this day. Of course, I'm a father of 3 as well. Alright. So we go back to our data model. Let's build out the rest of this. We've got our titles. So if we are creating a title now, like My Little Pony, now we can see what the status is. Is this a movie? Is it a series? What's the name, description? And we could pick a rating that we can then update through the system. And let's just adjust the display template for this because I wanna show the name anytime we're referencing that rating. Alright. So I'll zoom back in just a bit, and we'll build out our next piece of the puzzle here. Let's work on seasons. Alright. So we'll automatically generate a UUID. Maybe we wanna store who is the user that was updated this just so we can play the blame game later. Cool. Alright. And then we've got a number. So let's do an integer. We'll have the season number. Okay. And inside Directus, I can also make that the sort field. Where are you, sort field? Alright. So we'll choose the sort field. The season number is going to be the sort field. So whenever we reference this, it will sort by that number automatically. What else do we have? Do we have a description on the sort number? I I don't know. Release date, you know, this theoretically could be on the actual episodes as well. But Netflix does like to drop a season at a time, so maybe we store it here as well. Let's just use a time stamp so that it saves the the time zone value as well. Alright. What else? We have a title ID. Right? So we have to create that relationship back to the title so we can fetch that data. So this is gonna be a mini to 1. Right? A season can only belong, like, to a a single content title. So if if the show is My Little Pony, there's 5 seasons, all those seasons belong to 1 My Little Pony title. So we could call that title ID or title, I prefer less verbose, but it doesn't really matter here. Directus is going to serve up that however we create it. Alright. So we've got that. Let's add the one to many relationship. So we're creating a many to 1, but on the reverse side, a single title could have many seasons. So let's go ahead and create that relationship as well. And for the season we'll use the oh, for our many to 1 we'll use the name of that and then we'll do related values. Okay. So we got the title, we've got the number. You know, we could have a description for this season if we wanted to. You know, there's probably even, like, some collateral or trailers or or something attached to this. We won't worry about that right now. Let's save this, and then we'll go in and create episodes. So in our diagram over here, we probably should have had episodes to begin with. Alright. So same thing. I'll I'll just add my system field. So if anybody updates this information, we've got it. Is this episode published or not? Yes or no. And then we'll give this a name. What's the name of the episode? We probably also have a number for this episode as well. Episode number. And, you know, something like this, you may even call, like, episode number just to make sure everything is very clear, but I'm all for making things difficult. So we will add a description. And now we'll link this back to the season. Right? So we're going to use that many to one relationship again, and we're going to link this back to a season. The related collection is seasons. And I'm going to open up advanced field mode here in Directus, and I guess I could actually use my little mouse pose tool for you guys so we can get an idea of what's happening here. So I'm gonna add the reverse relationship back to seasons. So one season could have many episodes, 1 episode has or many episodes have a single season. Think I explained that right. Sounds great. Alright. So now we've got titles, we've got episodes, we've got seasons. If I look, we've got titles. So let's go in and create a new title. Right? So we'll just drag and drop Netflix over here. And let's just work on this. Right? My Little Pony, Make Your Mark. Alright. So this is My Little Pony, Make Your Mark. Alright. Maybe there's a hyphen there, a dot, I'm not sure. That's the season description. Right? Where do we see all episodes? Alright. So here's the there's kind of the the generic description that apparently Netflix does not let me copy. Alright. So welcome back to Equestria, where pony magic is everywhere. Who doesn't love pony magic? Alright. There's our description. This is TVY. Alright. And I can already see we've left out a couple things. Right? We've left out some thumbnail images, probably on each episode, each, title as well. But we'll go in and create a new season. They're calling this chapter 1. So we probably have a name for the season as well that we can add. Let's just say the release date is today, and we'll start adding some episodes. Right? So we got Make Your Mark. That's episode number 1. Something's wrong in Equestria. And now we're actually watching Netflix. How do I go back? Okay. Alright. Is there a way to actually I I guess they disable that. I'm just gonna use Raycast here to generate descriptions for this. Alright. So we'll say this is published. Alright. So at the very least, now we have some stuff that we can work with and fetch via the API that we can render on the front end. I'm gonna go back really quickly and just add a thumbnail for this title. And we could even do that for what, the episodes as well. We probably got, like, a thumbnail. There's probably also, like, a teaser. And while we're at it, let's just go ahead and try to finish out what we were building on the back end. Alright. So then we've got the actual content. So creating this separate collection allows me to standardize this across episode or, like, TV and movies. So, let's go in and create the content. You know, this could be video, I guess. Yeah. I'm not sure. Let's just call it content. And I'll probably figure out a better way to do this at some other point. Alright. So we got the content. We've got a just use a YouTube URL for this. We're gonna cheat a lot. But this could be like an actual file that we uploaded to our back end here that we serve. Again, I don't want to get into, like, the streaming stuff because I like efficiently serving video at different bandwidth, at different resolution. That's a whole ball of wax. I'm sure there's a great API for it. And Directus will manage some of those assets for you in a nice way, but it doesn't do video encoding and things like that that you need to deliver those streams effectively. Cool. So we've got, what, a YouTube URL. There's the content. This is gonna have a title associated with it. Titles. K. And then there's probably what? Episode as well? Episode. Titles. Okay. Alright. So we got a title. We got an episode. We probably should have created those reverse relationships, but we could go in and do that now. Alright. So we'll call this the content. That'll be our content collection. Episodes. And now we'll just create the reverse. Uh-oh. What am I doing wrong? I'm doing a mini to 1 back to that when, actually, let's just go in and I'm gonna delete these for now just to make sure we set these up correctly. Alright. So I'm gonna go let's go to the titles. We'll do a many to 1. We'll call this content. We'll go to the content collection. And here, I am going to go into the relationship. We'll add this to the title. Okay. So we'll add that reverse one to many relationship. We'll call it the title. I think it is going to be an array. I guess you could have multiple pieces of content attached to any one item, but no worries. Content. And then we're gonna have episode. Okay. Okay. Cool. Cool. Cool. Alright. What's next? Right? We probably need some genres. Let's go in genres. And if there could be multiple genres per piece of content, I assume, like, could be drama and horror or drama and faux drama. If you live in my household, I've got plenty of drama. So we'll give this a name. And then let's go in, and we would create a junction table between titles and genres. Right? But, again, one of the nice things here is Directus is gonna do a lot of this for us. So we could call this genres. We do genres as the related collection. And if we look, we go to the advanced mode inside Directus, we can see that there's gonna be a junction collection or a junction table in our database created. Do we want to be able to query by genres? Maybe we don't need that. Do we add a sort field? Probably not. Okay. So now we've got genres. We've got thumbnail. We got a lot of different items set up in this database. Right? And we're closing in on 30 minutes. We probably need to start building some UI soon. But I do wanna just open up the database itself and and take a look at what's going on now. So you'll see these direct us tables. But if you look, you can also see our our actual tables that we created, like episodes and genres. And if I look at the data, here's the episode that we created, make your mark for My Little Pony. So this is great. Like, anything that happens inside the app here, Directus is mirroring that to your database. Likewise, if I go in and, there's very robust access control here. I'm just gonna enable this as a public permission for all of our content, which, would probably not be a great thing security wise, but I just want to show you this. We'll go into local host 8055, which is my back end. And if I go to items and I go to titles, boom, I'm gonna get My Little Pony. Right? Now, this is not super helpful because we're just getting you know, like, let's say I I just wanted to make a single call and get all my content. I love Directus for that because it gives me an interface via REST that feels like GraphQL. So I get the benefits of GraphQL and then I can go in and do something like this, like, hey, I just want the ID, I want the type, I want the name, I want the description, Boom. And it will give me exactly what I asked for. Now, the other thing that I can also do is go in and if I want something like seasons, and I I could use a wildcard to grab all the root level fields of the the next one, right, but I can get all of my related content in a single API call. That's super powerful, super helpful. Obviously, like, the deeper you go, the slower the queries on the back end are gonna be, but it is still very speedy. And, obviously, I don't have a ton of data here. So we got seasons dot star, and then we could do seasons dot episodes dot star. And that should give us the episodes. And then once we add content, we could go even deeper and say something like seasons dot episodes dot, content dot star. Now there is no content at this point, but if there were, it would come back. Great. So that is just one way that one of the major things that I really love about directives. Alright. So we've got some stuff going on. Let's go in and populate a few more titles. Right? We've got a thumbnail. I don't think they're gonna let me lift this. So let's just do the old screenshot trick. Upload that image. Click to browse. Should actually just be able to drag and drop there. Okay. Let's create a new content for this. We'll call it YouTube. Pull you up, My Little Pony. And again, I I don't support any of this. Make sure anything that you are doing is okay legally. We'll just copy this. K. Great. We've got a movie. Actually, this is a series. Right? But one thing I noticed here is if I'm switching between movies and series, we should probably actually not show our seasons. Right? So how do we actually adjust that? Well, if we go into our titles, Directus has the ability to do, relational or, I'm sorry, conditional fields. So I could go in and for seasons, we could do something like this where we hide this by default, and then we come into our conditions tab. And if type equals series, so then we build a rule. This is just a description for this. We'll do type type equals Series, then we are going to make this not hidden. Right? And we'll show a link to the item. Did I get that right? Yep. Okay. Alright. So let's test this out just to see what's up. Let's go in and add another one. Right? And now where I have movie selected, nothing. But if I select series, now we're seeing that field. It doesn't change the underlying database structure or anything. It's just hiding that when I'm creating the content. But I could even add validation around it if I wanted to. So if we go in and we add a movie, let's see if we've got a movie. I could switch up here to the top to movies, and we open this up. We've got a trash truck Christmas. Christmas. Alright. This is TV Y, made for TV, made for TV goodness. Alright. We will create some new content for this. Where's our handy YouTube? Trash truck Christmas, full episode Netflix Junior. This looks like an official channel. Yeah. Okay. We'll see. So we got that. We've got a thumbnail image. Again, I'm not even gonna try to lift this from oh, actually, I I guess I could. Copy image address. Is that available? On the web? Okay. Cool. So we'll just lift these images directly. I can import that file. Great. There that is. And I could, you know, maybe potentially find their GraphQL, stream for this that allows me to pull all this in, in a a more timely fashion. But, okay. Genres. Right? Let's create a new genre. What are we calling this? This is kids TV. There's a new genre. Okay. We'll create a new kids and family movies. And this will be TV cartoons. Great. Okay. So we got a movie. Let's switch this back to a series. Right? Lots of series because we've got seasons attached to it. Got some content for it. Is it is that gonna be right? Actually, it's not, is it? We're going to go back to My Little Pony. We'll go into the seasons. We've got episodes, and we should have content for that episode. Cool. Alright. So let's just see what this all looks like. Great. I could even go in and adjust this to, like, something akin to Netflix if I wanted to where I could see, like, the thumbnails of all these different titles. So give it a name. The subtitles could be what? Description. What else do we have? Or it could also be can we get genres in here? Genre genres ID. Let me get the name of the genres. Cool. Alright. So we got a couple titles. We are what? We're pretty I'm pretty happy with where the back end is at at this point. You know, we could really go through this in a lot more detail and create something like actors, you know, the cast and crew, director, things like that. And those would probably all be different database tables. But for the sake of having something to display at the end of this next 28 minutes, let's just get to work on the front end. Alright? Cool. So we will let's put you back over here, Netflix. Close you. We'll close our database. Let's pull up our Nuxt application, and I'm just gonna swap these around. No real rhyme or reason. I just like my text editor being on the right side. Alright. So now let's pull up our Nuxt application. Oh, this thing is a beauty, right? So, I've just got a preconfigured, like, a Directus Nuxt module set up that basically just wraps the Directus SDK with a little bit of auth. You know, we may not even dive into that. But, and then I've got a use direct as composable that allows me to make requests easier. There is a a little bit of setup work that I've done here, but it is really just, one of our boiler plates HSC OS that I have taken and stripped back for the purposes here. Alright. So we go into our index page. Alright. So we let's look at, the homepage here of Netflix. And we've got kind of this setup of a header. We've got a grid of titles sorted by genre. Alright. So let's dive into this. The first thing that I'm going to do, just because we are short on time, is take a look at, Tailwind UI. Advanced. What's going on here? Okay. Let's just look at Nux UI as our our library here. Something wrong with my laptop at this point. Alright. So inside this boilerplate that I've got set up, I've got a little, I've got I've got this Nuxt UI library, which is kind of built on Tailwind as well, that gives us some nice things like modals, some of the trickier components to actually build. Right? So we go in. Let's try Tailwind UI again, now that I've refreshed a little bit. And, alright, so what are we looking for? Let's grab a, like, a header from Tailwind UI. Alright. This looks pretty good here. So we've got the view version. I'm just going to copy this down. Alright. And let's go in and create a header, error component header dot view. Alright. Are we gonna have a logo for this, right? We've got our logo. Can we pull that up? Will this let me copy as PNG? Can I export this as JPEG? Nope. Let's just do a new Figma file actually instead of FigJam. Can we paste that? If we copy, Can we paste? Devflix. Yeah. Alright. Let's give it some, like, type of little icon treatment here. So we'll add a real original logo here. Looks like a play button. We'll change the fill to red. Right? Okay. And maybe we, what, let's wrap this in. Let's give it a little oh, yeah. I'm I'm gonna be giving design courses after this as well. Alright. So we'll take this. We'll export to SVG. Let's just do logo. Great. Alright. So we've got this header component. If we go back to Netflix, what do we got at the top? We've got, home. That'll be our index page. This is for our navigation. Right? So we've got TV shows. Alright. We got movies. Great. Okay. Cool. I don't have Headless. I don't have heroicons in here, so let's just comment those out for now. Bars 3. I'm sure that'll throw a fit when we actually take a look at this. But cool. Alright. Now I'm gonna create a new file called logo dot view. And we're just gonna copy this as, what, SVG. Paste here. Nope. I need to wrap that in a template, though. Alright. There's our SVG. Now inside our header component, I'll go back up here to where we have Tailwind UI's logo. And I'm just going to call that logo component that we just had. Don't need a source. Don't need an alt tag. Well, we probably do, but not going to have one. 100 apps, 100 hours. Okay. So we have a header. We're not seeing it here. What I can do is adjust the default layout. And let's do something like this where we have BG gray 900 and probably text white since we're kinda going on the Netflix theme. So this is the layout inside Nux, which is a feature that I really like about Nux, is the ability to set up a default layout. And boom, we've got our DevFlix header looking nice. If we go back to that header, probably wanna swap these a tags or a Nuxt link and change this to 2. And we could change this to TV shows and movies. Right? We just have to create a route for those. And I'm not sure exactly how far we'll get because we have, what, 21 minutes remaining. But we'll we'll see how far we can get. Alright. So if I go into the header, we need to update the details for it as well, like bg white. What do we what does Netflix look like on the header? It's just kinda transparent until you scroll down. That's a nice effect. I don't want to set up anything there. So let's just cheat. We'll call it bggray900 again. And then that's gonna mess up a lot of our text, I believe. Right now, we can no longer see all of that. So we go to our text. Let's look at anything with text gray. Can we replace that with we could probably do smaller values, like text 200. Again, I love Tailwind just because it enables me to not have to write custom CSS for everything. Like, it it does take some time to learn the conventions, but but being able to not write CSS, I like custom CSS, just helps me speed along. Now I know it's not for everyone, so don't let me tell you that it is, But I enjoy it. Alright. Can we get our stuff? Okay. Alright. That's probably because we're missing an icon for our navigation over here. So what is this? This is hidden. Flex LG. Let's hide that at, like, just a smaller breakpoint. Yeah. Okay. Cool. Alright. So we got home. We got TV shows. We got movies. We can log in. Great. Now let's actually work on our page. Right? If we go in, let's actually render some stuff out. Alright. So if I look at Netflix, we've got a, like, a header component or, like, a hero image. So we go for our hero sections. Hero. Which one of these kinda looks like Netflix? Kind of this one? Kind of. I mean, this one has a background image. So we could take that. Okay. So we're not gonna copy this header piece, but let's just go in. I'm gonna copy this part. We will create a new one called hero dot view. I've got a just a couple of snippets already set up for me inside Versus Code to make creating Vue components a little easier. And k. We're good there. Watch chapter 6 now. Is this just kind of a rotating thing, probably based on, like, the last thing that you watched? Instead of doing that, let's do something else. Data. Alright. Alright. So this is gonna be justified center. We don't wanna do that. We wanna do does it justify start, I believe? K. Oh, we'll add this to our index page. So let's take a look at this. We'll just wrap this in a div. We'll go to hero, call in that component. Let's see what this is starting to look like. Alright. Okay. Okay. We got a little something going on here. I only succeeded in moving one thing to the left. So let's take a look at that hero component one more time. Alright. So this has got some type of gradient blur attached to it for the images. Can we lift this? Oh, yes, we can. Netflix. Love building on the fly. Thank you. Netflix. What else do we have? The overflow hidden. The the the announcing. Do we even need this? Let's let's just scrap that part of it. And we'll just show watch chapter 6 now. And I still can't copy this stuff. Come on, Netflix. Copy. Copy selector, copy element, edit as HTML. All right. So we'll just pick this up and throw it here. And we got the My Little Pony logo, so we'll add in Nuxt. We'll just add in an image tag because I don't have that domain configured. Image source. Alright. Class. I don't know. Maybe height 24. And an image tag, welcome back to Equestria Tech Center. This is not gonna be Tech Center. Okay. We're getting somewhere. How do we dim this image? Right? Brightness 50? Oh, wrong image, Brent. Brightness, 50. Maybe 25. Oh. Can we do a percentage here? Get, like, a a nice value. Okay. Alright. So we're getting closer. He likes it. Alright. Get started. Let's say watch now. Okay. Let's remove that one. Alright. So at least we got some type of header here. This would be a NuxLink. And, I'm trying to think of how we're gonna serve this content. Right? We would do a page. Let's add a new folder for this. This is gonna be what does Netflix do as far as routing? Right? If I open this up, episodes and info. Okay. So is it just like a genre? Is that how how the URL structure is? Because you watch Masha's spooky stories. So we got the genre. If I look at the URL again, yeah, it's just like Netlify Kids, and then there's a query param. Alright. So it actually appears to be inside the the actual application. Right? So we're just appending, like, a query per annum to it. Let's come back to that step. Let's actually render out some titles first, though. Right? So we go back to our page. Let's create a new component. What what do we gotta call this? Content grid? Sounds great. Vcomp ts. Okay. So now we've got a a Vue component. Let's do some data fetching. Right? We will do something like this where, we got constant data equals use async data. So this is the built in Nuxt data fetching. We give it a key. Let's call it content grid, comma. Alright. And then we're going to return something. So in this case, we're gonna use our directus composable that I've got inside this. But if not, I could import the Directus SDK, fire that up, but, we're on the on the clock here. So alright. So we're gonna read our items, and we are going to read titles. So and then we're gonna get, is there any specific thing that we wanna render out? Let's just leave our options blank for now. Right? So we got our data. Let's just go in and make sure we're getting that data back. Sounds great. I don't see it on the screen. Why not? That's because we're not actually including it in our index page. So we'll just do content grid. Great. Okay. So at least we could see a few things here. That's great. Do we really need anything else to render this grid? Probably not, actually. This shows, like, the viewing data. We're we're definitely not gonna get to that. We got 12 minutes left. Let's just see if we can actually render something out. It shows something in a modal. All right. So we got our content grid. Great. Cool. Let's go in. I'm not sure if Tailwind has a great looking component for this. Gridless. Kind of. Yeah. Let's just create something on the fly. Right? So we'll go in. Alright. So that's the wrapper div. Then we'll do, we would probably fetch this via genre as well. Again, running short on time. Alright. We are going to do a, let's just keep this simple. Right? Add add a little bit of padding to the sides. We'll give this a grid. Grid calls, 5 across the screen, 6 across the screen. And then we'll do, let's do a, what, title card? And we will pass so we'll do some v four action here. V 4 title and data. The key is gonna be title dot ID. And then we have our, what, We're going to pass a title, title equals title. All right. So now we're going to create a new component. Call it title card dot view. And we will define some props. So props equals define props, Title object is prop type. Haven't set up our types here even though we're still using TypeScript. Alright, so we've got a card. I know Telen has like a card definition. Actually, the Nuxt UI library has a card as well. Let's just use it. So we got a card. There's a card. U card is the name of this. U card. And what do we have inside the card? We have, I don't know if these actually need to be h tags or not. We'll just use a p tag. Got the title dot name. If I can actually type this. Right? We've got an image for the card. Source equals title. No, no, no, no. We're gonna use Nuxt image because I have it preconfigured. And I'll just show you that really quickly. If I go to Nuxt Config, ba ba ba ba, where are you? Where are you, mister Image? Image. Okay. So Nuxt Image Directus is a provider here. We just give it a base URL that communicates with our Directus instance and allows us to do image transformations on the fly, which is really nice. So we got source title dot thumbnail. K. What do we do? Like, what? With full height 24 maybe? Sounds good. We got a title. Let's make this font bold. I'm assuming that's what it looks like. Oh, no. It's actually just a card. Right? Alright. Let's see what this looks like. We'll go back to our index page, content grid. Why don't we have our content? Why is content grid not rendering anything? Do we save it? What's going on here? Dev server. Why are we not rendering? Title card. Content grid. There is no data. What are we doing there? We just had our data. Right? Am I being silly? Pre data. Is that not showing? Did I break something? What is going on? Use, async data, content grid, titles, read items. Where did you go? Did I not just have this, like, a minute ago? Title. Title equals title. Hydration mismatch. If I look at content grid, like, we should have data. Right? Let me just look at the Nuxt fetching. Sometimes I even I use Nuxt all the time, and I'm not sometimes I have to go back to the oh, we gotta wait. Yeah. I don't know how that worked the first time. Right? Next image not showing. Okay. So we're missing some images there. Why are we missing images? So we are missing images because, let's do let's add our grid back. Grid calls 6bg grid. Do we need an actual BG? Okay. Maybe grid calls 4. That's fine. How are we doing on the time? We got 6 minutes left to actually render something up. Px8py12. Just pad this out a little bit. Alright. What's next? Our our images are not displaying. Right? How do we fix that? It is actually gonna be inside our Directus instance. So it is a permissions issue. Pretty sure if I were to take a look at our network requests, look at it like images here, you have 403 forbidden. So, I would go into not my docs. We'll open up the Directus instance. Go to 8055. And I somehow managed to log myself out as well. Alright. We go into access control. We've got our content here, but then we're gonna go in and allow access to the direct us files collection. So that way we can actually serve those images on the front end. Okay. Great. Maybe title card wasn't a great idea there by using the Nux card. Right? Let's just call it div. Oh, forgot to close that. Alright. So there's the diff. This is going to be Object, Cover, Trash Truck Christmas. Maybe we need to make that a little higher, 32. Alright. How we doing on time? 4 minutes. Can we get this to actually render? Alright. Button at click. Alright. Quick and dirty. Let's do this. We're going to add a composables. Actually, we could just pass this up as well. Alright. At click, we're going to define some omits. So omits equals define omits. We're gonna tell this select title. Select title? I think that's the syntax. Alright. Alright. Select title. Emit. Select title. And we're gonna pass the what? Prop no. It should just be title dot ID. Is that right? Or does that do I have to wrap that? No. Where are we getting here? Select title does not exist. Let's just call this function. On click, emit select title, props dot title, on click. Cool. Okay. And then inside our content grid, let's do this where we've got a modal. K. And we've got is open. That's false. Select title. And then what are we gonna do for the title? Let's do title, maybe content. Right? Content ref equals null. GitHub Copilot here for the win on this one. Right? Do we have a video component? I don't have a video component. So we're just gonna do, what, an iframe, class, aspect, style. Let's call it video. We'll just add a quick style here. Dot video aspect ratio 16 by 9. Alright. The well, the source is gonna be the content dot YouTube URL. Is that actually gonna work? I think I've got do I have some strings in here? Do I have a video content? I don't. Alright. Let's rely on AI here. Generate YouTube embed. Okay. So that returns the video ID. Let's see what AI comes back with. YouTube embed video ID. Okay. Great. Alright. So there's gonna be our source. We'll say generate YouTube video embed URL. Oh, nope. That's YouTube embed URL. Like, somewhere in the starter, I may already have this. Generate YouTube embed URL, and that is gonna be the content dot YouTube URL. Alright. So what am I missing? I'm gonna need to close that. Okay. And then on our select title, right, we need to fetch the content for the title. Alright. So that's gonna be what? This is gonna be an async function now. How are we doing on time? We have hit time. So we ran out of time to actually flesh this out and render this content in a model, but I just wanna try it. Let's just pursue it really quickly and see where we're at. Constant. We are going to we don't need to actually use async data for this. We're gonna fetch this on the client side. So let's do constant title equals wait. Use direct us, read items, title. Read item. Is it gonna be a title? We're gonna fetch through the we got the root level fields. Let's take a look at that data model again. Right? Local hosts. Alright. So we got titles, then we've got content? Alright. So root level, we'll go content dot that. Select title. Title has already been declared. Oh, okay. Where do I have that at? Oh, I've already given it the title. Content. Alright. So if I click that, is it actually making the request? Select title. Oh, and then for the title card at select title, we're going to select title. Alright. So I can see that's making the request. I get the data here. There's our content. Do we have the YouTube URL? That's great. For some reason, it's not throwing the modal window. Right? Why is the modal not opening? Cannot read properties, undefined of YouTube URL. Alright. So we probably wrap this in, like, a computer or something. Selected content equals computed value. Alright. And this is going to be if selected content and we'll have selected content. Is that why? Alright. So it's actually throwing the modal, but why isn't that working? It's because I'm not passing the right stuff. Right? So it should be dot content dot YouTube URL. Is that gonna work? Where are you? Where are you? Modal. Title card. Where's the content grid? Our selected content. Content dot value. Oh, duh. Gosh. Forgetting to set the content. Content dot value equals content. Let's try it now. No. Still new. Okay. So I hate to waste everybody's time. Content dot oh, fetched content. Let's just try this real quick before I give this up. Equals fetched content. Holy moly. And there it is. Right? We have a Netflix clone. So a little after time, we made it actually render something, which is a win. Kudos to the Netflix team. Right? The truth of the matter is that it is very difficult to build Netflix in an hour. So I hope you enjoyed this episode. Like, what would happen next for me? Basically, continue to flesh this out, get the UI working and and wired up correctly. You know, if I wanted to copy Netlify or Netflix directly, I would probably make sure that when I loaded a title that was getting appended to the the actual query string in the URL. You might set it up on a different route that renders a model. But next steps would just be basically continuing down the route of fleshing out the the UI side of it and, you know, adjusting the data model inside our back end as needed. So that's it for this episode of 100 apps, 100 hours. The outcome, maybe we give it one thumbs up. You know, I'm sure Cisco and Evert would probably not approve. So I'll catch you on the next one. See you.","14af204b-9a61-4836-bdc5-e7698b851d34",[330],"58da3ed5-14f4-403d-94eb-3b3e2d10933e",[],{"id":172,"number":131,"show":122,"year":173,"episodes":333},[175,176,177,178,179,180,181,182,183,184,185],{"id":183,"slug":335,"vimeo_id":336,"description":337,"tile":338,"length":192,"resources":8,"people":339,"episode_number":341,"published":342,"title":343,"video_transcript_html":344,"video_transcript_text":345,"content":8,"seo":8,"status":130,"episode_people":346,"recommendations":348,"season":349},"food-delivery","908328732","Food delivery apps exploded during the pandemic. But what does it take to actually build one? Follow along with Bryant as he has 60 minutes on the clock to build a backend, create a menu, and place an order in his Doordash clone.","76299639-810f-4814-9b76-b714b16b1974",[340],{"name":199,"url":200},9,"2024-02-05","Mission: Food Delivery App","\u003Cp>Speaker 0: Hi guys. Welcome back to the next episode of 100 apps, 100 hours, where we rebuild or clone some of your favorite apps or publicly fail shamefully. Shamefully fail trying. Alright, I'm your host Brian Gillespie, developer advocate at Directus. Super excited to have you.\u003C/p>\u003Cp>I've got a special guest, my lovely wife joining me. I'm not sure if you can see her on my shirt here. This is a running gag between us where she basically gets me clothing with her face on it. So we'll include her in this episode. Today we're all about building a food ordering platform similar to DoorDash, Grubhub, or it seems like one of these platforms pops up every single week.\u003C/p>\u003Cp>The rules are very simple. We have 60 minutes to plan and build this application, no more, no less. And the second rule, the anti rule is we're gonna use whatever we have at our disposal, whether that's AI, UI frameworks, front end frameworks. We're going to be using Directus on the back end to generate this app really quickly. So let's kind of talk about the food ordering app before we dive in.\u003C/p>\u003Cp>You may have seen some of these before, we've just got a list of menu options, we want to be able to place those inside a cart and place an order for that cart, have somebody bring that food to us. So that's the basic gist of what we're building. Let's get started. Alright. So let's roll the timer and begin.\u003C/p>\u003Cp>I'm gonna just pull this to the side and let's pull up DoorDash just to take a look at this, right? So here's kind of what we're after. We've got different restaurants on here and if I click into one of these, like the bun stop shop, I have a menu of items here and we can add these to a particular cart, Choose some options. I don't know that we'll get to things like options. We'll probably just try to keep this very simple to begin with.\u003C/p>\u003Cp>We'll go ahead and add this to the cart and now I can go and place an order for this and have it sent to my delivery location. So great. That looks good. Let's discuss the functionality that we actually want out of this particular application, right? So I'm going to drag this down.\u003C/p>\u003Cp>In 60 minutes, I don't wanna bite off more than I can chew here, if we're doing cliches. So let's, you know, we've got the data model for the back end. We'll get that set up. On the front end we will show a list of menu items for restaurants. Allow place an order for menu items, and that's probably pretty good for an hour.\u003C/p>\u003Cp>I don't know. We'll see how far we get with it. The next thing I always like to do is to sketch out my data model. And one way that you could do this is obviously if you're trying to model another application, right, you could always kind of look at it and think through it. One of the things that I like to do, in this case I'm gonna just search for the DoorDash API because I know that they have other services that connect to this.\u003C/p>\u003Cp>And it looks like they do have an API that we could potentially use here. We don't wanna drive for DoorDash. Maybe it's this marketplace API, how to guides, menu flows and order flows. Here we go. Retrieve orders from DoorDash, set up a menu pull, set up a menu push, get a DoorDash menu.\u003C/p>\u003Cp>Okay. Yeah. So I can at least see a little bit of how they've got this structured, right? So I see there's a store that's associated to this. We have a menu.\u003C/p>\u003Cp>I'm assuming this is like a could be a breakfast menu or a lunch menu or a dinner menu. Maybe we have different menus that way. Then we have categories, I see, and then we have the actual items. Alright. So that gives me a good idea of of how they've solved this problem.\u003C/p>\u003Cp>And obviously, they're a large company, so they've got a lot of smart people on it. Do I want to take that at face value? Probably not. But for an hour, it seems like a pretty good model. Right?\u003C/p>\u003Cp>So let's start sketching this out. I'm just gonna make this full screen. We'll add a little box here. We've got, stores or restaurants. I'm not sure which one of those I'm gonna call it yet.\u003C/p>\u003Cp>Probably stores is easier. Then we have a we have menus. What else do we have underneath that? We've got menu items. Probably like categories.\u003C/p>\u003Cp>Right? I saw that on there. So we've got different items in the categories. Menu items, and then we're gonna have an orders table with probably some order items as well, orders items. Alright.\u003C/p>\u003Cp>Don't necessarily have to do this, but, yeah. Each menu belongs to a restaurant. The menu items belong to a category. So there's a relationship there. Just draw these arrows.\u003C/p>\u003Cp>This is more for me just visually to see how everything maps. Directus, our back end will make creating these relationships and these models very easy. Great. We probably have a restaurant that's attached to the order. We have a menu item that's attached to the order item.\u003C/p>\u003Cp>And then we've got, an actual person placing the order. Directus is gonna give us that. That will be our users table probably. User, users place orders. Okay.\u003C/p>\u003Cp>Alright. So as far as the structure of this, we got that. Don't worry about that little guy there. This seems pretty good as far as the structure. Right?\u003C/p>\u003Cp>So how do we start building something like this? I'm gonna pull up my blank instance of Directus. Directus is going to mirror all the changes that we make here inside the application, all of our collections. Those are going to be tables inside our database. All the fields are going to get represented as columns.\u003C/p>\u003Cp>So it's a great way to build out this functionality. And for me, I'm very front end oriented, so I'm a very visual person. So instead of writing migrations, this is great for me. I can go in and build this data model visually. And if I need to store it as code I can kick out the schema dot JSON file as well that others who are working on this project could apply.\u003C/p>\u003Cp>So let's go with stores. I really like that. If we just take a look at DoorDash, one of the things that I was curious about, okay. It looks like we have an ID for the store. They're not using anything like a slug.\u003C/p>\u003Cp>So it doesn't really matter for SEO purposes. Let me get back here. So we're gonna create this new collection. I can zoom in a bit. We've got stores.\u003C/p>\u003Cp>We've got our generated UUID. Let's go ahead and and just add these system fields here, just for status, date created. You know, they give us some of that functionality out of the box. So we're gonna have a name for the store. We're gonna keep this really lightweight so we could try to get, push orders into this.\u003C/p>\u003Cp>So we'll just do a name. Let's do a logo or an image. Let's just call it image for the store. Great. We probably got something like an address, probably.\u003C/p>\u003Cp>Right? Street address. We have a city. I'm just gonna duplicate this field, which is really nice. Street is a city, state, or region.\u003C/p>\u003Cp>We're trying to be international friendly here. And then we'll have postal code. Great. Cool. And then I may even just like group these together inside Directus using our detail group interface.\u003C/p>\u003Cp>We'll just call it address info. And we can even add a nice little map pin. There we go. So we'll group these together. Region and city, we can make those half width.\u003C/p>\u003Cp>Directus really allows you to customize the forms that your users will see. You know, on the front end here, I'm probably not going to give the people placing orders the access to the Directus Data Studio here. But, I certainly could give the restaurants owners or the store owners access to that to manage their menu, their store information. Alright, so we've got, this looks pretty good for our stores. Let's move on to the next one.\u003C/p>\u003Cp>Actually, take that back. Let me add a building, like a company, Office building. Building. Business. There we go.\u003C/p>\u003Cp>Alright. So that looks kind of like a store. Great. Alright. So next we are going to add menus.\u003C/p>\u003Cp>Alright. We will, I can again, I can add these things whether they are in draft mode or publish. You know, I may want to track when it was created, who it was updated by, etcetera. We'll give a name for the menu. And I'm just going to skirt the categories thing here, and just add these.\u003C/p>\u003Cp>Maybe we don't. But, yeah, let's do. We'll just go menu items. Right. And for menu items, again, I'll just populate these.\u003C/p>\u003Cp>They're always easy to remove later. We might want to have a sort field so we can order those items, not not actually place an order for them, but order them in terms of like sorting. What else? Okay. So on our menu items, if we look at DoorDash, we've got we've got a name for the item.\u003C/p>\u003Cp>We've got, description. Probably a price. Alright. Where's our DoorDash menu? What does an item have?\u003C/p>\u003Cp>An item has a name, it has a description, is it active, is it alcohol, is it bike friendly? I would argue that a cheeseburger is not bike friendly, but I'm sure this is on the delivery side. And then I see things like price, I see a tax rate, and then I see, like, some extras. For the purposes of this, let's keep it lightweight. We've got a name for this.\u003C/p>\u003Cp>We've got an image image for the menu item. We'll just create that. Let's have a description. I'm just gonna keep that as straight text. Toggle the sidebar within Arc as well.\u003C/p>\u003Cp>So you probably don't wanna do a WYSIWYG description here. And there's always risk if you're not sanitizing HTML content that is user submitted. So we'll just use text area. What else do we have? We have a price that we're going to set for this.\u003C/p>\u003Cp>That can be an input. I I know a lot of ecommerce options store the prices as integers, like incense. I'm just gonna use a decimal here And I could go into field creation mode if I want to, if I wanna control like the precision and things like that, and the scale. So I will just put 2 here. I only want 2 decimal places.\u003C/p>\u003Cp>We could do a formatted label and I can even add a prefix within Directus, which is really nice here. We'll do auto format, see what that generates for us. Cool. So we've got our menu items, we've got menus. Alright.\u003C/p>\u003Cp>Let's add a relationship between all of these. So we have a store, it has a menu or potentially menus, and then each menu has menu items. So if I go into menus, the designer or the recovering designer in me is gonna freak out if I do not add at least some type of icon for this. But let's add a relationship and this is gonna be really easy inside Directus. This is gonna be a one to many relationship because we have items on our menu.\u003C/p>\u003Cp>We have one menu, many items. So I'll go in and we'll call this, items is going to be the name of our field in the menu collection or the menus table and then our related collection is going to be menu items and the foreign key would be the menu. Right? So inside that menu items we're going to create a new field called menu. And I definitely want to show a link back to that item within the data studio.\u003C/p>\u003Cp>Now if you expand this into the advanced settings when you're creating relationships or just adding fields to the collection, you can see here that menu is not on menu items. That field does not currently exist. But Directus is gonna create that for us, which is really nice. Right? I don't have to mess with sitting down and doing migrations.\u003C/p>\u003Cp>As you can see, we're gonna build this functionality really quickly. Alright. So we have menus, we have a name, we have menu items. Let's go ahead and save this. If I go into our menu items, now I've got a field for menu, so that's going to show up.\u003C/p>\u003Cp>What else do we need to add here? Right? We're gonna need an orders table. So we'll say orders, we'll definitely wanna know who placed those orders, what's the status of that order, is it ready or not. And then let's go in and add order items.\u003C/p>\u003Cp>Again, naming things is very challenging, you know. Should this be called orders underscore items or order items? I like it this way. Totally up to you. Whatever you wanna name this inside your own.\u003C/p>\u003Cp>Maybe we sort those. So we've got orders, we've got order items. The order, if we just think through this, and of course add a great looking icon for it, the order is gonna be linked to a specific store. Right? We can only place one order from a store.\u003C/p>\u003Cp>So we'll go in, this is gonna be a mini to one relationship, and we've got this up over here so we can see everything we've got going on. This is gonna be the store. The related collection will be our stores, and Directus is going to create that relationship for us. And again, if I were to just open up Table Plus, so you can get a look at this. What is happening behind the scenes here?\u003C/p>\u003Cp>Directus is creating these relationships, it's creating my tables, it's creating all my columns in the database and it's also creating those relationships for me inside the underlying SQL. So if I were to rip out Directus one day, you know all my SQL data is still pure everything inside my database. Alright, so we've got an order, we've got a store, we've got a, a customer. Right? We'll call that customer.\u003C/p>\u003Cp>This is gonna be the Directus users collection. We've got an address for that so we could go in and do street address, city. I can also go back to that other collection and just duplicate these across, which is probably what I should have done. But no worries there. Alright.\u003C/p>\u003Cp>So we'll add these fields and again, I could group these together using our detail group. We'll call this address info. You know, we may again, we're gonna have a front end where users place their order, but on the back end we could fulfill these orders through the Directus interface as well, through the app. And that again that helps me just move things along faster so that I don't have to build an admin interface. I could just use Directus here to manage all of that data.\u003C/p>\u003Cp>Alright. Orders and then we have our items, right? So we'll go in this is again, this is gonna be a one to many relationship. There are one order or there's one order that has many items. Because I know yeah.\u003C/p>\u003Cp>At least at first, or when you're modeling data this way, it can be kind of confusing of which relationships to use. So this is a one to many from the orders table to the order items. I'm gonna call this items as well, and the table is gonna be order items, and then we're gonna use a foreign key of order. I always like to show a link to the item so I can open that up inside Directus and we'll just hit save here. Great.\u003C/p>\u003Cp>We got a store, we got a customer, we got a street address, we got a status that we can go in and adjust. Alright. This is a draft order, published order, an archived order. That doesn't make a ton of sense. Alright.\u003C/p>\u003Cp>So let's just nuke those. We'll call this a new order. We'll call it, In Progress or Being Made. I don't know. Again, naming things is part of the one of the hardest things inside development.\u003C/p>\u003Cp>We'll say ready for pickup for pickup. And what? Delivery? I'm in out for delivery. Delivery.\u003C/p>\u003Cp>Out for delivery. Out for delivery. Let's just say delivered. Right? I think those are just a quick rundown of what the different states could be for a specific order.\u003C/p>\u003Cp>Is it new? It's being made? It's ready for pickup? Somebody comes and picks it up, it's out for delivery, and it's delivered. Right.\u003C/p>\u003Cp>Again, it's not as simple as this. Right? You've got a driver and things like that included, but we're keeping this very simple so we can actually dive in to the functionality. So on the order, we also probably have like a total for the order as well. Right?\u003C/p>\u003Cp>Order total Order total that would sum up all of the individual line items. This is gonna be an input field. The schema though, we probably want the type to be decimal, just because that's what we have for our menu items. Great. Again, you could store this as synths and you know, do all of that on the front end as well.\u003C/p>\u003Cp>What do we what else do we have? We've got the order items, we got the order, and then we're gonna add a relationship here to the menu items. So this is gonna be a mini to one relationship because we can only add 1 menu item to one order item. That's great. So we'll call this the menu item.\u003C/p>\u003Cp>Sounds great. Menu items. That's our related collection. We'll hit save. What do we have next?\u003C/p>\u003Cp>We probably have a price for this specific item as well. Again, that's gonna be a decimal. I can adjust the number of decimal points. Great. And then one of the other things that I saw on DoorDash here, this is not a an actual order.\u003C/p>\u003Cp>Let's see if we can find a an order. Right. What does an actual order look like? Receive orders from DoorDash, blah blah blah, notable fields. Let's just look at the UI.\u003C/p>\u003Cp>So I go in and I add this. I can add special instructions. Right? So let's add a field for special instructions. And we also probably have a quantity.\u003C/p>\u003Cp>Right? I can go in and adjust the quantity of these. So if I want 3, 4, 5 hash browns, I don't have to add a separate item. So we'll do quantity. That'll be an integer.\u003C/p>\u003Cp>You can order half of a hash brown. I guess you could. I'm not sure that they would deliver it. So we have quantity, menu item. Let's show the actual order.\u003C/p>\u003Cp>This little hidden I there tells me that that is not displaying to the person when they pull up this form. But there we have the order, we have the menu item, we have the quantity, we have the price, and we probably have an something like amount or subtotal, item subtotal. Again, this is where it gets tricky as to what you name these things. So the subtotal here would just be the quantity times the price, that's going to equal our item subtotal. Alright.\u003C/p>\u003Cp>So this feels pretty pretty good, right? That's, kind of what we had. We just totally erased categories for now. We just wanna get to this core functionality. We've got, roughly 30 minutes and some change to evaluate this, right?\u003C/p>\u003Cp>So first, let's go in. I'm just gonna pull this up side by side, and I'm just gonna steal some of these items. Right? Let's go in and I I don't wanna do McDonald's. I've got my address set to Yankee Stadium here in the Bronx.\u003C/p>\u003Cp>If you work at Yankee Stadium, I'm sorry. We're gonna deliver some food to you. May not be something that you like. Alright. Let's go with wallet friendly.\u003C/p>\u003Cp>Jimbo's Jimbo's Hamburger House. Looks good. Let's go in and add a new store over here. So inside Directus, you can see how that form, that data model that I set up, how this actually looks when we are managing this. So again, you're probably for something like DoorDash or an order delivery app, you're probably not going to give the person placing the order, like the end user, access to this.\u003C/p>\u003Cp>But this interface is so beautiful, I could certainly give the store owners that wanted to sell their stuff on our platform access to this. So this is 252 Saint Ann's Avenue, Bronx, New York. I don't have a postal code, we'll just say 5555. Right. And then I can see I've got a image here.\u003C/p>\u003Cp>I can just copy that image address and Directus makes this really easy to import images from a URL. Boom. Great. Maybe I even wanna show this image. When you are creating your data models, you've got a ton of options as to how these things display inside the application.\u003C/p>\u003Cp>So I could go into this image field and I could display a tiny image preview. I can even make it show up as a circle like what they have there. Great. There we go. We've got Jenbo's Hamburger Stand, Hamburger House.\u003C/p>\u003Cp>Let's go into our menus. Right? So we'll create a menu. Let's call this the lunch menu. We're missing that relationship though.\u003C/p>\u003Cp>Right? Where did we did we actually create a relationship between menus and stores? We did not. So let's do that quickly before we get too carried away. We will create a many to one relationship on the menus back to the store.\u003C/p>\u003Cp>Stores, and then I can open this up and in our relationship field, I can add that back as well. So here's our menus. Cool. Great. Alright.\u003C/p>\u003Cp>So now I've got that relationship. I go into the menu, I can pick the store, there's Jenbos hamburgers. And let's start adding some items from this. Right? We've got our silver dollar pancakes.\u003C/p>\u003Cp>This looks good. Items are silver dollar pancakes. I can copy that image address and have direct us pull this in. We've got some additional options here. I don't see any like descriptions for those.\u003C/p>\u003Cp>Not a big deal. The price is 6.50. So we'll just input that. Alright. Next, let's add some other options.\u003C/p>\u003Cp>We've got a Philly steak bacon wrap with fries deluxe. So we'll just add that here. Looks like I don't have an image for that one, but I do have a description. What is the price for that? It is 12.50.\u003C/p>\u003Cp>Great. So we'll just input some of this data. Why do I have an image on this? Jenbo, you need to get some images on DoorDash, my man. So we'll just do a Philly steak and cheese.\u003C/p>\u003Cp>Who knows where we're ripping this from? This looks good. Copy image address. That one's not available. Philly steaks.\u003C/p>\u003Cp>I guess this is gonna be all sorts of copyright infringement here. This is for entertainment purposes only. Any lawyers watching this, just so we know. Maybe I could get Nat. Nat edits our videos.\u003C/p>\u003Cp>He's amazing. Nat, maybe you could place a disclaimer on here somewhere. Alright. So we've got a couple of menu items. What else do we have?\u003C/p>\u003Cp>We have waffles. We have tex mex. Good enough. Alright. So we don't have any orders.\u003C/p>\u003Cp>We don't have any order items, but at least we have a couple menu items and we have a store. Great. Let's dive into something on the front end. Right? What am I using on the front end?\u003C/p>\u003Cp>I've got a just a basic Nuxt starter kit. I'm with you guys, so I I like coding in Nuxt and Vue. And then I've just got this simple app. Right? So if we go back to DoorDash, we've got to get some kind of view where we have a list of restaurants.\u003C/p>\u003Cp>And how do we do this? Right. Nux has a Nux 3 has this use async data composable. So let's just call this stores, that's our key. And then inside this starter kit, I've just got a module that communicates with Directus.\u003C/p>\u003Cp>I've got a little composable here that auto imports items from our, like the different methods from our SDK and allows me to communicate. So we'll just adjust this. This is going to be our stores. And here, let's just maybe, we're not gonna flex this. But let's just log our data and see what we're getting back.\u003C/p>\u003Cp>So I could do data. Maybe I wrap this in a pre tag. Bada bing bada boom. I should be able to get this data. We should be reading something from stores, right?\u003C/p>\u003Cp>I'm not. So the issue here is that I have not set any permissions. Directus comes with rule based access control, which is pretty amazing that I can configure all this via the UI and it applies in real time. But we've got 2 roles by default. The administrator role, which I can apply to users who have full access to the data model, all the different settings, and then we have the public role.\u003C/p>\u003Cp>So the public role, we want to give access to, probably not So they could probably see the stores, they could see the menus and the menu items. We don't want to give them ability to see any of the orders or create orders. Right? You want to log in for that. So we might as well go ahead and add a new user here as well.\u003C/p>\u003Cp>So the user role should be able to place orders, they should be able to view orders, with the exception that they can only see their orders. Right? We don't want them to create menu items, but one of the other nice things about the rule based access control in Directus is I could set up custom permissions. Right? If I am a user, I I shouldn't be able to see everybody else's orders, I should just be able to see mine.\u003C/p>\u003Cp>So I could do something like this, where in this read permission setting for our orders collection, I could go in and control the items that are available. Right? So if I've got a user that created this, has to equal dollar sign underscore current user. So basically this is a little syntactic sugar. This will, fetch the current user ID for you and set up permissions so that if I'm logged in as a specific user, I can only see my orders.\u003C/p>\u003Cp>That's super nice. Alright. Cool. Let's go back to our front end. Now I refresh.\u003C/p>\u003Cp>I can actually see some data. This is good. Right. Now let's go in and actually flesh this out. We've got div, maybe we've got a grid of, what, 3 columns.\u003C/p>\u003Cp>I I love using Tailwind for the styling. Grid calls 3, maybe a gap of 4. Alright. And then we have I'm using the Nuxt UI library just, as a cheat codes here. They have a card component that I can use here.\u003C/p>\u003Cp>We'll just say ucard. We'll do a v 4. And it looks like, GitHub Copilot is propelling me forward here. We have the store dot ID. What are we gonna drop inside the actual card?\u003C/p>\u003Cp>We probably have the what do we wanna do here? We wanna add the image up top, and I can see there's like a template, like a header slot and a base slot, but maybe we just stick it all in the same one. I'm also using Nuxt Image in this as well, which is really nice because, I don't have to create any it will automatically optimize the images for me. And Directus will do that via the API as well, but this is kind of nice just because they use the same underlying technology, the same library. So let's do height 48, width 48, object cover, Jinbo.\u003C/p>\u003Cp>We're not getting the actual image. Right? What's going on here? So if I check our network requests, if I just slide this over to images, we're getting a 403 forbidden on the images as well. Right?\u003C/p>\u003Cp>So images and assets are a system collection inside Directus. So I've got Directus files, that's where all of our assets get stored, aside from the actual image on disk, we're keeping that library of them. I forgot to enable control for those. So I can go in under system collections, I'll just enable access to those files. Let's go ahead and give this user access to those as well.\u003C/p>\u003Cp>Where are you? Files. Okay. They've got access. That's great.\u003C/p>\u003Cp>I probably shouldn't have closed Directus out entirely. But now I could see I've got this, right. And for the width, let's just do with full instead of 48. Okay. Object cover.\u003C/p>\u003Cp>It's a little pixelated. It doesn't look great. But, next let's go in and add but we're going to add a title for this, right, the store dot name. Let's make this bold. And maybe 3xl.\u003C/p>\u003Cp>Jinbo's hamburgerhouse. Okay. Alright. Up top here, we can add a h one tag, restaurants or h 2. Or maybe this is a h one.\u003C/p>\u003Cp>Bold text gray 600. Maybe we add a bit of margin to that. It's a little much, maybe mb 8. And then here for this, I think we've got a container here as well that we might use. I think it just applies some padding.\u003C/p>\u003Cp>You container. So we get away from Okay. Yeah. Now we're not pressing on the edges of this. Another one of my favorite tools is Tailwind UI.\u003C/p>\u003Cp>If you build apps and you like speed running things like I do, super handy, right. I could go in and I've got store navigation here. I could just basically lift one of these store navigation components if I wanted to. So let's look for like a header marketing elements. We've got a header here.\u003C/p>\u003Cp>How do I just get something really simple? This looks pretty good. We'll just copy this. I'm gonna create a new component. I'm gonna call it the header dot view.\u003C/p>\u003Cp>We'll do a view component. What do they got? They've already got some of this for me. Let's just copy it and see how far we get. I could go into our default layout and maybe I want to add the header here.\u003C/p>\u003Cp>See what that gets us. Failed to resolve the imports for the icons. They're using a different icon set than I am. I'm gonna delete these out. And I'm not even really super concerned with this navigation either.\u003C/p>\u003Cp>Let's just make this our cart. We'll change that to a button for now. Type equals button. What do we get? Are we getting anything?\u003C/p>\u003Cp>Yeah. Cart MX button. Why is it not showing that? Oh, this is probably the mobile menu button. Hidden flex type gray.\u003C/p>\u003Cp>Justify end. Is it not showing? Okay. There it is. Product login.\u003C/p>\u003Cp>Let's do something like MD here. That way this is not hidden. Empty. Refresh. Okay.\u003C/p>\u003Cp>So I got login. Let's just change this to cart. Again, I'm gonna change this quickly to a button because we are going to use it to display our cart at some point. Alright. Do we even need product?\u003C/p>\u003Cp>Let's just remove that. Forget about it. Okay. So we've got, we got a menu or we've got a restaurant. Right?\u003C/p>\u003Cp>Now we need to be able to click on this specific restaurant. We go into that restaurant and then we're gonna display a list of all the items from that specific restaurant. Alright. So let's go in and create a new route. We'll call it restaurants or stores in this case.\u003C/p>\u003Cp>And inside that directory, I'm gonna do something like this where I use, what are those called, just brackets? Yeah. We'll just use a dynamic route here, vcomp ts. And I could copy this async data call here. And let's call this our actual store.\u003C/p>\u003Cp>So we've got our stores. We're gonna read. We don't wanna read all the stores. We just wanna pick up 1 individual store. And we're gonna do that via the route parameters.\u003C/p>\u003Cp>So we'll do this route equals use route, which is just a composable that ships with view 3. And here should be route dot ID, and then as the third argument we can pass in, you know, like, hey, I want all these specific fields or not. Again, anytime I'm just like testing this out, I may add something like pre and just log that data just to see what I am working with initially. Okay. So we'll go back to our index page.\u003C/p>\u003Cp>We wanna make this, clickable. Alright. So maybe we wrap this in Nuxt link. The 2 is gonna be, let's say, dash stores store dot ID. Alright.\u003C/p>\u003Cp>Let's see where that gets us. Now I can click on this and it goes to the store. Right? You can see that we had a route change. Store, for the let's do something like this.\u003C/p>\u003Cp>Async data for the the caching, it uses a key. So I could pass the key like this and do route dot ID. Let's see if that gives us what we're after. I refresh. Why am I not getting this?\u003C/p>\u003Cp>Oh, route dot params.id. Duh. Silly rabbit tricks are for kids. Alright. So here's the data for the individual store.\u003C/p>\u003Cp>Alright. If I go back, you can see the difference. I'm getting an array of items here because I'm calling the Directus API via read items. When I click on Jenbo's hamburger house, I get just the actual item because we are using read item here. We're picking that up by the param ID.\u003C/p>\u003Cp>Now, how do I get access to this actual menu item? Right. A couple ways I could do this. I could make another call or one of the many things that I love about Directus is the ability to get all the related fields in a single call. And I'm going to use this wild card syntax here just so we're we could take a look at it.\u003C/p>\u003Cp>In production, you probably wouldn't want to use this. But if I wanted to, I could just fetch the ID. Right? So it's very GraphQL like and then I can tell it specifically the fields that I want and I can go in and do something like this where I get menus dot star. We can see okay, here's the the different menus.\u003C/p>\u003Cp>I want to drill down one more layer and I want to get menus dot items dot star. That is a wild card for all of these items and that could give me everything that I need to render this on the page. Right? I could go one further if I'm storing like metadata, like alt tags or title text or something on the image. Itself, I could drill into that further.\u003C/p>\u003Cp>Really all I need to render an image is just the ID, but great. So now how do we set this up? Let's go in and we'll add a menu. Let's add an h one. This is gonna be the store dot name.\u003C/p>\u003Cp>Class 3xl. Font bold. Maybe we don't make this bold. Let's see what we got. We got Gino's hamburger's house.\u003C/p>\u003Cp>Maybe we dress this up a little bit. What does it look like inside door dash? Got like this nice header image up here at the top. We don't really have one of those, but I can at least show the, logo for the store. Right?\u003C/p>\u003Cp>Let's make it look nice. Store dot image. Is that what we called it? Class let's make it square. 48.\u003C/p>\u003Cp>Let's do 24. We'll make it smaller. Right. With 24, object cover rounded full, this should give us the image there. We could probably wrap this in, div.\u003C/p>\u003Cp>Just flex that. Class equals flex to a gap of 2. And then within this, we might wrap that in a div as well. Okay. Cool enough.\u003C/p>\u003Cp>We've got that. Now let's add these actual menu items. Right? So I've got this array of menu items. Let's just take a peek at how we're doing on time.\u003C/p>\u003Cp>Roughly a little less than 20 minutes. Are we gonna get this done or not? We shall see. Right? So I'll go in.\u003C/p>\u003Cp>Let's just do a grid of menu items. Great. Got a gap. Go co Copilot go. Right?\u003C/p>\u003Cp>We're use that same ucard component. V for menu and store menu. I really like GitHub Copilot. It's great for like really simple auto completion things like this. One of the things that I can tell you is make sure whatever you do, you verify all of this before you actually use it.\u003C/p>\u003Cp>Right? Because here I'm getting okay. This is not actually working at all. So v 4 menu and store menus, we really just want the first item. Right?\u003C/p>\u003Cp>We're gonna do the items in store menus 1, the first item in there. That's gonna be item dot ID. We don't really need the store. Right. This is kinda weird.\u003C/p>\u003Cp>This is where GitHub Copilot gets you into, a lot of trouble if you just blindly do whatever it's it's saying here. And this is still not coming up right. Item in store dot menus dot 1. This would be the actual menus dot items. There we go.\u003C/p>\u003Cp>Okay. So now we could see we've got our silver dollar pancakes, we've got our Philly steak and cheese. Let's make this smaller. And then at the bottom, we're going to add our price. Don't forget the item dot description.\u003C/p>\u003Cp>Then we also need a price. Alright. So item dot price. Let's just see what that looks like. We can add a dollar sign.\u003C/p>\u003Cp>We've got our silver dollar pancakes. Cool. Maybe we wanna give a little bit of gap between all these. This is not gonna be a Nuxt link either, right. We want this to be a button type equals button.\u003C/p>\u003Cp>We'll add some functionality into this in a moment. Class equals text left. Okay. Maybe we give it a flex call gap 2, Give a little bit of spacing in there. And make the width full.\u003C/p>\u003Cp>Well that, yeah, there we go. That gets us what we want. Let's make the gap larger. Okay. Cool.\u003C/p>\u003Cp>So we got some menu items. Right? We want to click on these menu items and add them into a cart. So what are we gonna do, how are we gonna manage this cart? Because it could be we want to maintain this cart across like page load and also across different restaurants or different pages as well.\u003C/p>\u003Cp>So for now, I think I'm just going to simply use a composable for this. I don't have Pina installed, which is a really great state library for Vue. You know, Nuxt has a built in used state composable that you could potentially use as well. But I like the idea of just adding a composable to manage this. So I'm gonna add a new folder here in the root directory.\u003C/p>\u003Cp>I'm gonna call it composable Composables. Nuxt will actually auto import these. And let's call it use cart dot ts. Alright. We're going to export a default function, use cart.\u003C/p>\u003Cp>And up here at the top, let's just take a look at our cart structure. Right. What do we have? What do we want to send? So we've got the orders.\u003C/p>\u003Cp>If we take a look at our data model, right, we have the store, we have the customer, we have that's gonna be our individual user. So we've got to log in as a user. We got our items. We got a total for that. Okay.\u003C/p>\u003Cp>So let's go in and add a list of items. That's gonna be an array of items. Right. What else are we going to have inside this composable? Right.\u003C/p>\u003Cp>We've got our items. We're pulling this outside of the function definition because we want to store this. If we call this use cart in multiple places, we still wanna access the same items. Alright. So this is gonna return the items for that cart.\u003C/p>\u003Cp>We're gonna add those cart the order total. Alright. So we have the order total that's going to be computed. That would be a return items dot for dues dot item dot price. That's perfect.\u003C/p>\u003Cp>Thank you, Github. Got an order total. And then we've probably got a function for, oh, let's go back up, async function, place, yeah. Let's add to cart. We're just gonna push an item to the cart.\u003C/p>\u003Cp>We gotta remove from cart. Look at GitHub Copilot. Again, like, please take this with a grain of salt, whether this is actually wise or not. Index of item, items value dot splice. Index self item.\u003C/p>\u003Cp>Okay. And then if we've got, and these actually probably don't need to be async functions. And then we probably got an async function for submit order. Alright. Submit order.\u003C/p>\u003Cp>Alright. This is gonna be constant data equals I don't know if that's gonna be data or not. We'll do await. We'll do use directus. Then we are going to create an item in the orders table or orders collection, the total for that order total, ordertotal will be the order total dot value.\u003C/p>\u003Cp>Then we have our items, which are going to be, should just be items. Alright. So we could do something like, nope. We're not gonna do that. We are going to do unref items since that is unrefed.\u003C/p>\u003Cp>We've got the store info and this customer field, inside direct us, I can save the current user on create. So let's do that. So the store is going to be where are we gonna get the store from? Maybe we're gonna pass that in the function. Store ID, that's a string.\u003C/p>\u003Cp>Store ID. Alright. Is this cool? We're gonna return these other functions from our composable. Let's see how we do.\u003C/p>\u003Cp>Alright. So now if we go back to our app, we've got the hamburger house. Let's just remove this, comment that out. Alright. So we got our menu, We want to push these items into a cart.\u003C/p>\u003Cp>Where are we at time wise? We got 10 minutes left. Are we gonna actually get these into a cart or not? So here inside this specific, page we might do what we are going to return. Let's take a look at our composable.\u003C/p>\u003Cp>Right. We've got our order items. We've got our items in the cart. Let's call the we're gonna get items. Let's call these cart items.\u003C/p>\u003Cp>Add to cart. Remove from cart equals used cart. K. And then can we hey. Let's just drop in our cart items here just to see what that looks like.\u003C/p>\u003Cp>Cart items. It's It's not showing anything for our card items. Are we getting an error? Let's actually take a look. So we'll go to ID.\u003C/p>\u003Cp>Cart items equals undefined. Oh, it's because I'm getting items. There we go. Alright. So we're showing just an array.\u003C/p>\u003Cp>Let's just do this. I'm just gonna give this some padding. BG gray, 50. Let's make it 100. This is our, p tag cart items.\u003C/p>\u003Cp>Okay. Alright. So now what we want to do on each one of these, right, if we click an add to cart button, which we we don't necessarily have. Where is that? Right?\u003C/p>\u003Cp>Why do we not have a button for add to cart? Maybe we don't even put that. That shouldn't even be in here basically. So let's move this up. We'll just take the class.\u003C/p>\u003Cp>We'll apply that to the card itself. Why is it not doing what we want? Gapspacey2. Why are you not doing what I want you to do? Alright.\u003C/p>\u003Cp>I'm not gonna stress over it. We'll just wrap it in a div because this will be the div task adventure. I know all the non tailwind CSS guys are probably freaking out at this moment, but okay. We'll use this u button component provided by Nuxt UI and we're going to click add to cart. We're going to add that item to the cart.\u003C/p>\u003Cp>Yeah. Close enough. Right. So if I click add to cart, we can see we've got our cart item here. It's not necessarily what I want.\u003C/p>\u003Cp>Right? The data structure is not entirely what I want. So let's actually just define it here. The item ID, we want this to be our order items. Alright.\u003C/p>\u003Cp>So we go into order items, We got the menu item. Let's do that. Menu item. Okay. Special instructions.\u003C/p>\u003Cp>No onions, please. Yeah. We'll just automatically populate that. The quantity, let's set that to 1. And then the item subtotal, we'll just say item price.\u003C/p>\u003Cp>Alright. So now if I refresh, does that give us kind of the structure that we want? It's still populating the entire menu item. We just want the menu item item.id. That should give us what we need.\u003C/p>\u003Cp>Okay. Does that match the right structure? Great. Where we at on time? We got, 5 minutes or so.\u003C/p>\u003Cp>Let's actually get this order together. Right? Alright. So inside our header, maybe we could display our cart items there. I really wanted to actually show the submitting to Directus.\u003C/p>\u003Cp>So knowing that I am short on time, right, we could go in and add these items. Let's just try submitting it right now and see. Alright. So we got our cart items. Let's go in.\u003C/p>\u003Cp>We've got new button, place order, and we'll call this submit order. Are we passing we're passing that store ID as well. Alright. So this isn't probably the best particular type of design, but it will get the job done. Let's do a large button.\u003C/p>\u003Cp>At click is submit order and then we've got the store dot ID that we're passing to this. And then let's go in and just like what does our submit order look like? This should be probably like try catch. So we could catch any type of errors. Response.\u003C/p>\u003Cp>If we catch an error, we want to return the response. Great. Submit order. Oh, that looks a little rough. Let's move that over here.\u003C/p>\u003Cp>Oh, I put I did I totally put that in the wrong spot anyway. Submit order and let's just hide this if the user is not logged in as well. Right? We don't want them to be able to place an order if you are not logged in. So this composable that I have, I want to say, has a or like this module that I set up has some of this already built in.\u003C/p>\u003Cp>So this will be v Where's the directus auth? Directus auth, user. So we'll do something like this where we get constant user equals use direct us off if there is a user. Okay. Alright.\u003C/p>\u003Cp>And if there's not a user, let's do a u button. V of no user, we are going to I think I could do this with it, where we have login to place an order. Login to order. And we will do what is it? 2 auth, I don't even need that.\u003C/p>\u003Cp>We do to auth/auth/login. Login to order. Okay. We've got our user. I didn't set up a user, but let's create one now.\u003C/p>\u003Cp>This will be our user. Test usertest@example.com. Let's give them a real secure password. We'll do password here and then I am going to select the role of user. Great.\u003C/p>\u003Cp>Save it. Now I should actually be able to log in. So test at example dot com, Password of that login. Tried to log in to the wrong spot. Where's the redirect for this?\u003C/p>\u003Cp>Use direct us off. Is this going to be in my Nuxt config? We want to redirect to auth login, not portal. We want to redirect to the index page. Oh, we're gonna we're gonna be cutting this one really really close.\u003C/p>\u003Cp>Come on. Alright. So we go back. Do we have we've got our menus. We don't have any cart orders.\u003C/p>\u003Cp>It looks like I'm still am I still logged in? Do we have the cookie? We've got okay. So we've got a session token here, so it's still showing me as logged in. If we hit add menu items, I hit place order, what happens is we can see the order posted.\u003C/p>\u003Cp>If we go inside Directus, do we have that specific order? Yes, we do. So we've got here's the store information, here's the order, we've got the item subtotal. We don't have the prices not being populated. So I didn't item subtotal should be item dot price.\u003C/p>\u003Cp>Why is that not populating correctly? I didn't add the price either. But item dot price. Cool. I'm calling this a win.\u003C/p>\u003Cp>So we've got what have we done? We've set up our data model within an hour. We've got all the items coming through. I could go in and clean this up, certainly, and that would be my next steps. But we've got a really simple UI here on the front end that with probably another 20, 30 minutes and a little bit of love.\u003C/p>\u003Cp>Man, this thing would be amazing. So I can go in, if I refresh this page, we should probably ditch that, but I can go in and add my pancakes. I can place the order and see it show up inside Directus. Alright. So we just do a quick review.\u003C/p>\u003Cp>What did we achieve? Right. We got the data model done, check. We got a list of the menu items for a restaurant, check. And then we got the place an order for menu items.\u003C/p>\u003Cp>Boom. Knock all 3 of those off. I'm gonna credit that to my wife being here to help me today. That's it for this episode, guys. I hope you've enjoyed this one.\u003C/p>\u003Cp>If you did, make sure you hit the subscribe button on Directus TV because we want you to catch more of this content. See you.\u003C/p>","Hi guys. Welcome back to the next episode of 100 apps, 100 hours, where we rebuild or clone some of your favorite apps or publicly fail shamefully. Shamefully fail trying. Alright, I'm your host Brian Gillespie, developer advocate at Directus. Super excited to have you. I've got a special guest, my lovely wife joining me. I'm not sure if you can see her on my shirt here. This is a running gag between us where she basically gets me clothing with her face on it. So we'll include her in this episode. Today we're all about building a food ordering platform similar to DoorDash, Grubhub, or it seems like one of these platforms pops up every single week. The rules are very simple. We have 60 minutes to plan and build this application, no more, no less. And the second rule, the anti rule is we're gonna use whatever we have at our disposal, whether that's AI, UI frameworks, front end frameworks. We're going to be using Directus on the back end to generate this app really quickly. So let's kind of talk about the food ordering app before we dive in. You may have seen some of these before, we've just got a list of menu options, we want to be able to place those inside a cart and place an order for that cart, have somebody bring that food to us. So that's the basic gist of what we're building. Let's get started. Alright. So let's roll the timer and begin. I'm gonna just pull this to the side and let's pull up DoorDash just to take a look at this, right? So here's kind of what we're after. We've got different restaurants on here and if I click into one of these, like the bun stop shop, I have a menu of items here and we can add these to a particular cart, Choose some options. I don't know that we'll get to things like options. We'll probably just try to keep this very simple to begin with. We'll go ahead and add this to the cart and now I can go and place an order for this and have it sent to my delivery location. So great. That looks good. Let's discuss the functionality that we actually want out of this particular application, right? So I'm going to drag this down. In 60 minutes, I don't wanna bite off more than I can chew here, if we're doing cliches. So let's, you know, we've got the data model for the back end. We'll get that set up. On the front end we will show a list of menu items for restaurants. Allow place an order for menu items, and that's probably pretty good for an hour. I don't know. We'll see how far we get with it. The next thing I always like to do is to sketch out my data model. And one way that you could do this is obviously if you're trying to model another application, right, you could always kind of look at it and think through it. One of the things that I like to do, in this case I'm gonna just search for the DoorDash API because I know that they have other services that connect to this. And it looks like they do have an API that we could potentially use here. We don't wanna drive for DoorDash. Maybe it's this marketplace API, how to guides, menu flows and order flows. Here we go. Retrieve orders from DoorDash, set up a menu pull, set up a menu push, get a DoorDash menu. Okay. Yeah. So I can at least see a little bit of how they've got this structured, right? So I see there's a store that's associated to this. We have a menu. I'm assuming this is like a could be a breakfast menu or a lunch menu or a dinner menu. Maybe we have different menus that way. Then we have categories, I see, and then we have the actual items. Alright. So that gives me a good idea of of how they've solved this problem. And obviously, they're a large company, so they've got a lot of smart people on it. Do I want to take that at face value? Probably not. But for an hour, it seems like a pretty good model. Right? So let's start sketching this out. I'm just gonna make this full screen. We'll add a little box here. We've got, stores or restaurants. I'm not sure which one of those I'm gonna call it yet. Probably stores is easier. Then we have a we have menus. What else do we have underneath that? We've got menu items. Probably like categories. Right? I saw that on there. So we've got different items in the categories. Menu items, and then we're gonna have an orders table with probably some order items as well, orders items. Alright. Don't necessarily have to do this, but, yeah. Each menu belongs to a restaurant. The menu items belong to a category. So there's a relationship there. Just draw these arrows. This is more for me just visually to see how everything maps. Directus, our back end will make creating these relationships and these models very easy. Great. We probably have a restaurant that's attached to the order. We have a menu item that's attached to the order item. And then we've got, an actual person placing the order. Directus is gonna give us that. That will be our users table probably. User, users place orders. Okay. Alright. So as far as the structure of this, we got that. Don't worry about that little guy there. This seems pretty good as far as the structure. Right? So how do we start building something like this? I'm gonna pull up my blank instance of Directus. Directus is going to mirror all the changes that we make here inside the application, all of our collections. Those are going to be tables inside our database. All the fields are going to get represented as columns. So it's a great way to build out this functionality. And for me, I'm very front end oriented, so I'm a very visual person. So instead of writing migrations, this is great for me. I can go in and build this data model visually. And if I need to store it as code I can kick out the schema dot JSON file as well that others who are working on this project could apply. So let's go with stores. I really like that. If we just take a look at DoorDash, one of the things that I was curious about, okay. It looks like we have an ID for the store. They're not using anything like a slug. So it doesn't really matter for SEO purposes. Let me get back here. So we're gonna create this new collection. I can zoom in a bit. We've got stores. We've got our generated UUID. Let's go ahead and and just add these system fields here, just for status, date created. You know, they give us some of that functionality out of the box. So we're gonna have a name for the store. We're gonna keep this really lightweight so we could try to get, push orders into this. So we'll just do a name. Let's do a logo or an image. Let's just call it image for the store. Great. We probably got something like an address, probably. Right? Street address. We have a city. I'm just gonna duplicate this field, which is really nice. Street is a city, state, or region. We're trying to be international friendly here. And then we'll have postal code. Great. Cool. And then I may even just like group these together inside Directus using our detail group interface. We'll just call it address info. And we can even add a nice little map pin. There we go. So we'll group these together. Region and city, we can make those half width. Directus really allows you to customize the forms that your users will see. You know, on the front end here, I'm probably not going to give the people placing orders the access to the Directus Data Studio here. But, I certainly could give the restaurants owners or the store owners access to that to manage their menu, their store information. Alright, so we've got, this looks pretty good for our stores. Let's move on to the next one. Actually, take that back. Let me add a building, like a company, Office building. Building. Business. There we go. Alright. So that looks kind of like a store. Great. Alright. So next we are going to add menus. Alright. We will, I can again, I can add these things whether they are in draft mode or publish. You know, I may want to track when it was created, who it was updated by, etcetera. We'll give a name for the menu. And I'm just going to skirt the categories thing here, and just add these. Maybe we don't. But, yeah, let's do. We'll just go menu items. Right. And for menu items, again, I'll just populate these. They're always easy to remove later. We might want to have a sort field so we can order those items, not not actually place an order for them, but order them in terms of like sorting. What else? Okay. So on our menu items, if we look at DoorDash, we've got we've got a name for the item. We've got, description. Probably a price. Alright. Where's our DoorDash menu? What does an item have? An item has a name, it has a description, is it active, is it alcohol, is it bike friendly? I would argue that a cheeseburger is not bike friendly, but I'm sure this is on the delivery side. And then I see things like price, I see a tax rate, and then I see, like, some extras. For the purposes of this, let's keep it lightweight. We've got a name for this. We've got an image image for the menu item. We'll just create that. Let's have a description. I'm just gonna keep that as straight text. Toggle the sidebar within Arc as well. So you probably don't wanna do a WYSIWYG description here. And there's always risk if you're not sanitizing HTML content that is user submitted. So we'll just use text area. What else do we have? We have a price that we're going to set for this. That can be an input. I I know a lot of ecommerce options store the prices as integers, like incense. I'm just gonna use a decimal here And I could go into field creation mode if I want to, if I wanna control like the precision and things like that, and the scale. So I will just put 2 here. I only want 2 decimal places. We could do a formatted label and I can even add a prefix within Directus, which is really nice here. We'll do auto format, see what that generates for us. Cool. So we've got our menu items, we've got menus. Alright. Let's add a relationship between all of these. So we have a store, it has a menu or potentially menus, and then each menu has menu items. So if I go into menus, the designer or the recovering designer in me is gonna freak out if I do not add at least some type of icon for this. But let's add a relationship and this is gonna be really easy inside Directus. This is gonna be a one to many relationship because we have items on our menu. We have one menu, many items. So I'll go in and we'll call this, items is going to be the name of our field in the menu collection or the menus table and then our related collection is going to be menu items and the foreign key would be the menu. Right? So inside that menu items we're going to create a new field called menu. And I definitely want to show a link back to that item within the data studio. Now if you expand this into the advanced settings when you're creating relationships or just adding fields to the collection, you can see here that menu is not on menu items. That field does not currently exist. But Directus is gonna create that for us, which is really nice. Right? I don't have to mess with sitting down and doing migrations. As you can see, we're gonna build this functionality really quickly. Alright. So we have menus, we have a name, we have menu items. Let's go ahead and save this. If I go into our menu items, now I've got a field for menu, so that's going to show up. What else do we need to add here? Right? We're gonna need an orders table. So we'll say orders, we'll definitely wanna know who placed those orders, what's the status of that order, is it ready or not. And then let's go in and add order items. Again, naming things is very challenging, you know. Should this be called orders underscore items or order items? I like it this way. Totally up to you. Whatever you wanna name this inside your own. Maybe we sort those. So we've got orders, we've got order items. The order, if we just think through this, and of course add a great looking icon for it, the order is gonna be linked to a specific store. Right? We can only place one order from a store. So we'll go in, this is gonna be a mini to one relationship, and we've got this up over here so we can see everything we've got going on. This is gonna be the store. The related collection will be our stores, and Directus is going to create that relationship for us. And again, if I were to just open up Table Plus, so you can get a look at this. What is happening behind the scenes here? Directus is creating these relationships, it's creating my tables, it's creating all my columns in the database and it's also creating those relationships for me inside the underlying SQL. So if I were to rip out Directus one day, you know all my SQL data is still pure everything inside my database. Alright, so we've got an order, we've got a store, we've got a, a customer. Right? We'll call that customer. This is gonna be the Directus users collection. We've got an address for that so we could go in and do street address, city. I can also go back to that other collection and just duplicate these across, which is probably what I should have done. But no worries there. Alright. So we'll add these fields and again, I could group these together using our detail group. We'll call this address info. You know, we may again, we're gonna have a front end where users place their order, but on the back end we could fulfill these orders through the Directus interface as well, through the app. And that again that helps me just move things along faster so that I don't have to build an admin interface. I could just use Directus here to manage all of that data. Alright. Orders and then we have our items, right? So we'll go in this is again, this is gonna be a one to many relationship. There are one order or there's one order that has many items. Because I know yeah. At least at first, or when you're modeling data this way, it can be kind of confusing of which relationships to use. So this is a one to many from the orders table to the order items. I'm gonna call this items as well, and the table is gonna be order items, and then we're gonna use a foreign key of order. I always like to show a link to the item so I can open that up inside Directus and we'll just hit save here. Great. We got a store, we got a customer, we got a street address, we got a status that we can go in and adjust. Alright. This is a draft order, published order, an archived order. That doesn't make a ton of sense. Alright. So let's just nuke those. We'll call this a new order. We'll call it, In Progress or Being Made. I don't know. Again, naming things is part of the one of the hardest things inside development. We'll say ready for pickup for pickup. And what? Delivery? I'm in out for delivery. Delivery. Out for delivery. Out for delivery. Let's just say delivered. Right? I think those are just a quick rundown of what the different states could be for a specific order. Is it new? It's being made? It's ready for pickup? Somebody comes and picks it up, it's out for delivery, and it's delivered. Right. Again, it's not as simple as this. Right? You've got a driver and things like that included, but we're keeping this very simple so we can actually dive in to the functionality. So on the order, we also probably have like a total for the order as well. Right? Order total Order total that would sum up all of the individual line items. This is gonna be an input field. The schema though, we probably want the type to be decimal, just because that's what we have for our menu items. Great. Again, you could store this as synths and you know, do all of that on the front end as well. What do we what else do we have? We've got the order items, we got the order, and then we're gonna add a relationship here to the menu items. So this is gonna be a mini to one relationship because we can only add 1 menu item to one order item. That's great. So we'll call this the menu item. Sounds great. Menu items. That's our related collection. We'll hit save. What do we have next? We probably have a price for this specific item as well. Again, that's gonna be a decimal. I can adjust the number of decimal points. Great. And then one of the other things that I saw on DoorDash here, this is not a an actual order. Let's see if we can find a an order. Right. What does an actual order look like? Receive orders from DoorDash, blah blah blah, notable fields. Let's just look at the UI. So I go in and I add this. I can add special instructions. Right? So let's add a field for special instructions. And we also probably have a quantity. Right? I can go in and adjust the quantity of these. So if I want 3, 4, 5 hash browns, I don't have to add a separate item. So we'll do quantity. That'll be an integer. You can order half of a hash brown. I guess you could. I'm not sure that they would deliver it. So we have quantity, menu item. Let's show the actual order. This little hidden I there tells me that that is not displaying to the person when they pull up this form. But there we have the order, we have the menu item, we have the quantity, we have the price, and we probably have an something like amount or subtotal, item subtotal. Again, this is where it gets tricky as to what you name these things. So the subtotal here would just be the quantity times the price, that's going to equal our item subtotal. Alright. So this feels pretty pretty good, right? That's, kind of what we had. We just totally erased categories for now. We just wanna get to this core functionality. We've got, roughly 30 minutes and some change to evaluate this, right? So first, let's go in. I'm just gonna pull this up side by side, and I'm just gonna steal some of these items. Right? Let's go in and I I don't wanna do McDonald's. I've got my address set to Yankee Stadium here in the Bronx. If you work at Yankee Stadium, I'm sorry. We're gonna deliver some food to you. May not be something that you like. Alright. Let's go with wallet friendly. Jimbo's Jimbo's Hamburger House. Looks good. Let's go in and add a new store over here. So inside Directus, you can see how that form, that data model that I set up, how this actually looks when we are managing this. So again, you're probably for something like DoorDash or an order delivery app, you're probably not going to give the person placing the order, like the end user, access to this. But this interface is so beautiful, I could certainly give the store owners that wanted to sell their stuff on our platform access to this. So this is 252 Saint Ann's Avenue, Bronx, New York. I don't have a postal code, we'll just say 5555. Right. And then I can see I've got a image here. I can just copy that image address and Directus makes this really easy to import images from a URL. Boom. Great. Maybe I even wanna show this image. When you are creating your data models, you've got a ton of options as to how these things display inside the application. So I could go into this image field and I could display a tiny image preview. I can even make it show up as a circle like what they have there. Great. There we go. We've got Jenbo's Hamburger Stand, Hamburger House. Let's go into our menus. Right? So we'll create a menu. Let's call this the lunch menu. We're missing that relationship though. Right? Where did we did we actually create a relationship between menus and stores? We did not. So let's do that quickly before we get too carried away. We will create a many to one relationship on the menus back to the store. Stores, and then I can open this up and in our relationship field, I can add that back as well. So here's our menus. Cool. Great. Alright. So now I've got that relationship. I go into the menu, I can pick the store, there's Jenbos hamburgers. And let's start adding some items from this. Right? We've got our silver dollar pancakes. This looks good. Items are silver dollar pancakes. I can copy that image address and have direct us pull this in. We've got some additional options here. I don't see any like descriptions for those. Not a big deal. The price is 6.50. So we'll just input that. Alright. Next, let's add some other options. We've got a Philly steak bacon wrap with fries deluxe. So we'll just add that here. Looks like I don't have an image for that one, but I do have a description. What is the price for that? It is 12.50. Great. So we'll just input some of this data. Why do I have an image on this? Jenbo, you need to get some images on DoorDash, my man. So we'll just do a Philly steak and cheese. Who knows where we're ripping this from? This looks good. Copy image address. That one's not available. Philly steaks. I guess this is gonna be all sorts of copyright infringement here. This is for entertainment purposes only. Any lawyers watching this, just so we know. Maybe I could get Nat. Nat edits our videos. He's amazing. Nat, maybe you could place a disclaimer on here somewhere. Alright. So we've got a couple of menu items. What else do we have? We have waffles. We have tex mex. Good enough. Alright. So we don't have any orders. We don't have any order items, but at least we have a couple menu items and we have a store. Great. Let's dive into something on the front end. Right? What am I using on the front end? I've got a just a basic Nuxt starter kit. I'm with you guys, so I I like coding in Nuxt and Vue. And then I've just got this simple app. Right? So if we go back to DoorDash, we've got to get some kind of view where we have a list of restaurants. And how do we do this? Right. Nux has a Nux 3 has this use async data composable. So let's just call this stores, that's our key. And then inside this starter kit, I've just got a module that communicates with Directus. I've got a little composable here that auto imports items from our, like the different methods from our SDK and allows me to communicate. So we'll just adjust this. This is going to be our stores. And here, let's just maybe, we're not gonna flex this. But let's just log our data and see what we're getting back. So I could do data. Maybe I wrap this in a pre tag. Bada bing bada boom. I should be able to get this data. We should be reading something from stores, right? I'm not. So the issue here is that I have not set any permissions. Directus comes with rule based access control, which is pretty amazing that I can configure all this via the UI and it applies in real time. But we've got 2 roles by default. The administrator role, which I can apply to users who have full access to the data model, all the different settings, and then we have the public role. So the public role, we want to give access to, probably not So they could probably see the stores, they could see the menus and the menu items. We don't want to give them ability to see any of the orders or create orders. Right? You want to log in for that. So we might as well go ahead and add a new user here as well. So the user role should be able to place orders, they should be able to view orders, with the exception that they can only see their orders. Right? We don't want them to create menu items, but one of the other nice things about the rule based access control in Directus is I could set up custom permissions. Right? If I am a user, I I shouldn't be able to see everybody else's orders, I should just be able to see mine. So I could do something like this, where in this read permission setting for our orders collection, I could go in and control the items that are available. Right? So if I've got a user that created this, has to equal dollar sign underscore current user. So basically this is a little syntactic sugar. This will, fetch the current user ID for you and set up permissions so that if I'm logged in as a specific user, I can only see my orders. That's super nice. Alright. Cool. Let's go back to our front end. Now I refresh. I can actually see some data. This is good. Right. Now let's go in and actually flesh this out. We've got div, maybe we've got a grid of, what, 3 columns. I I love using Tailwind for the styling. Grid calls 3, maybe a gap of 4. Alright. And then we have I'm using the Nuxt UI library just, as a cheat codes here. They have a card component that I can use here. We'll just say ucard. We'll do a v 4. And it looks like, GitHub Copilot is propelling me forward here. We have the store dot ID. What are we gonna drop inside the actual card? We probably have the what do we wanna do here? We wanna add the image up top, and I can see there's like a template, like a header slot and a base slot, but maybe we just stick it all in the same one. I'm also using Nuxt Image in this as well, which is really nice because, I don't have to create any it will automatically optimize the images for me. And Directus will do that via the API as well, but this is kind of nice just because they use the same underlying technology, the same library. So let's do height 48, width 48, object cover, Jinbo. We're not getting the actual image. Right? What's going on here? So if I check our network requests, if I just slide this over to images, we're getting a 403 forbidden on the images as well. Right? So images and assets are a system collection inside Directus. So I've got Directus files, that's where all of our assets get stored, aside from the actual image on disk, we're keeping that library of them. I forgot to enable control for those. So I can go in under system collections, I'll just enable access to those files. Let's go ahead and give this user access to those as well. Where are you? Files. Okay. They've got access. That's great. I probably shouldn't have closed Directus out entirely. But now I could see I've got this, right. And for the width, let's just do with full instead of 48. Okay. Object cover. It's a little pixelated. It doesn't look great. But, next let's go in and add but we're going to add a title for this, right, the store dot name. Let's make this bold. And maybe 3xl. Jinbo's hamburgerhouse. Okay. Alright. Up top here, we can add a h one tag, restaurants or h 2. Or maybe this is a h one. Bold text gray 600. Maybe we add a bit of margin to that. It's a little much, maybe mb 8. And then here for this, I think we've got a container here as well that we might use. I think it just applies some padding. You container. So we get away from Okay. Yeah. Now we're not pressing on the edges of this. Another one of my favorite tools is Tailwind UI. If you build apps and you like speed running things like I do, super handy, right. I could go in and I've got store navigation here. I could just basically lift one of these store navigation components if I wanted to. So let's look for like a header marketing elements. We've got a header here. How do I just get something really simple? This looks pretty good. We'll just copy this. I'm gonna create a new component. I'm gonna call it the header dot view. We'll do a view component. What do they got? They've already got some of this for me. Let's just copy it and see how far we get. I could go into our default layout and maybe I want to add the header here. See what that gets us. Failed to resolve the imports for the icons. They're using a different icon set than I am. I'm gonna delete these out. And I'm not even really super concerned with this navigation either. Let's just make this our cart. We'll change that to a button for now. Type equals button. What do we get? Are we getting anything? Yeah. Cart MX button. Why is it not showing that? Oh, this is probably the mobile menu button. Hidden flex type gray. Justify end. Is it not showing? Okay. There it is. Product login. Let's do something like MD here. That way this is not hidden. Empty. Refresh. Okay. So I got login. Let's just change this to cart. Again, I'm gonna change this quickly to a button because we are going to use it to display our cart at some point. Alright. Do we even need product? Let's just remove that. Forget about it. Okay. So we've got, we got a menu or we've got a restaurant. Right? Now we need to be able to click on this specific restaurant. We go into that restaurant and then we're gonna display a list of all the items from that specific restaurant. Alright. So let's go in and create a new route. We'll call it restaurants or stores in this case. And inside that directory, I'm gonna do something like this where I use, what are those called, just brackets? Yeah. We'll just use a dynamic route here, vcomp ts. And I could copy this async data call here. And let's call this our actual store. So we've got our stores. We're gonna read. We don't wanna read all the stores. We just wanna pick up 1 individual store. And we're gonna do that via the route parameters. So we'll do this route equals use route, which is just a composable that ships with view 3. And here should be route dot ID, and then as the third argument we can pass in, you know, like, hey, I want all these specific fields or not. Again, anytime I'm just like testing this out, I may add something like pre and just log that data just to see what I am working with initially. Okay. So we'll go back to our index page. We wanna make this, clickable. Alright. So maybe we wrap this in Nuxt link. The 2 is gonna be, let's say, dash stores store dot ID. Alright. Let's see where that gets us. Now I can click on this and it goes to the store. Right? You can see that we had a route change. Store, for the let's do something like this. Async data for the the caching, it uses a key. So I could pass the key like this and do route dot ID. Let's see if that gives us what we're after. I refresh. Why am I not getting this? Oh, route dot params.id. Duh. Silly rabbit tricks are for kids. Alright. So here's the data for the individual store. Alright. If I go back, you can see the difference. I'm getting an array of items here because I'm calling the Directus API via read items. When I click on Jenbo's hamburger house, I get just the actual item because we are using read item here. We're picking that up by the param ID. Now, how do I get access to this actual menu item? Right. A couple ways I could do this. I could make another call or one of the many things that I love about Directus is the ability to get all the related fields in a single call. And I'm going to use this wild card syntax here just so we're we could take a look at it. In production, you probably wouldn't want to use this. But if I wanted to, I could just fetch the ID. Right? So it's very GraphQL like and then I can tell it specifically the fields that I want and I can go in and do something like this where I get menus dot star. We can see okay, here's the the different menus. I want to drill down one more layer and I want to get menus dot items dot star. That is a wild card for all of these items and that could give me everything that I need to render this on the page. Right? I could go one further if I'm storing like metadata, like alt tags or title text or something on the image. Itself, I could drill into that further. Really all I need to render an image is just the ID, but great. So now how do we set this up? Let's go in and we'll add a menu. Let's add an h one. This is gonna be the store dot name. Class 3xl. Font bold. Maybe we don't make this bold. Let's see what we got. We got Gino's hamburger's house. Maybe we dress this up a little bit. What does it look like inside door dash? Got like this nice header image up here at the top. We don't really have one of those, but I can at least show the, logo for the store. Right? Let's make it look nice. Store dot image. Is that what we called it? Class let's make it square. 48. Let's do 24. We'll make it smaller. Right. With 24, object cover rounded full, this should give us the image there. We could probably wrap this in, div. Just flex that. Class equals flex to a gap of 2. And then within this, we might wrap that in a div as well. Okay. Cool enough. We've got that. Now let's add these actual menu items. Right? So I've got this array of menu items. Let's just take a peek at how we're doing on time. Roughly a little less than 20 minutes. Are we gonna get this done or not? We shall see. Right? So I'll go in. Let's just do a grid of menu items. Great. Got a gap. Go co Copilot go. Right? We're use that same ucard component. V for menu and store menu. I really like GitHub Copilot. It's great for like really simple auto completion things like this. One of the things that I can tell you is make sure whatever you do, you verify all of this before you actually use it. Right? Because here I'm getting okay. This is not actually working at all. So v 4 menu and store menus, we really just want the first item. Right? We're gonna do the items in store menus 1, the first item in there. That's gonna be item dot ID. We don't really need the store. Right. This is kinda weird. This is where GitHub Copilot gets you into, a lot of trouble if you just blindly do whatever it's it's saying here. And this is still not coming up right. Item in store dot menus dot 1. This would be the actual menus dot items. There we go. Okay. So now we could see we've got our silver dollar pancakes, we've got our Philly steak and cheese. Let's make this smaller. And then at the bottom, we're going to add our price. Don't forget the item dot description. Then we also need a price. Alright. So item dot price. Let's just see what that looks like. We can add a dollar sign. We've got our silver dollar pancakes. Cool. Maybe we wanna give a little bit of gap between all these. This is not gonna be a Nuxt link either, right. We want this to be a button type equals button. We'll add some functionality into this in a moment. Class equals text left. Okay. Maybe we give it a flex call gap 2, Give a little bit of spacing in there. And make the width full. Well that, yeah, there we go. That gets us what we want. Let's make the gap larger. Okay. Cool. So we got some menu items. Right? We want to click on these menu items and add them into a cart. So what are we gonna do, how are we gonna manage this cart? Because it could be we want to maintain this cart across like page load and also across different restaurants or different pages as well. So for now, I think I'm just going to simply use a composable for this. I don't have Pina installed, which is a really great state library for Vue. You know, Nuxt has a built in used state composable that you could potentially use as well. But I like the idea of just adding a composable to manage this. So I'm gonna add a new folder here in the root directory. I'm gonna call it composable Composables. Nuxt will actually auto import these. And let's call it use cart dot ts. Alright. We're going to export a default function, use cart. And up here at the top, let's just take a look at our cart structure. Right. What do we have? What do we want to send? So we've got the orders. If we take a look at our data model, right, we have the store, we have the customer, we have that's gonna be our individual user. So we've got to log in as a user. We got our items. We got a total for that. Okay. So let's go in and add a list of items. That's gonna be an array of items. Right. What else are we going to have inside this composable? Right. We've got our items. We're pulling this outside of the function definition because we want to store this. If we call this use cart in multiple places, we still wanna access the same items. Alright. So this is gonna return the items for that cart. We're gonna add those cart the order total. Alright. So we have the order total that's going to be computed. That would be a return items dot for dues dot item dot price. That's perfect. Thank you, Github. Got an order total. And then we've probably got a function for, oh, let's go back up, async function, place, yeah. Let's add to cart. We're just gonna push an item to the cart. We gotta remove from cart. Look at GitHub Copilot. Again, like, please take this with a grain of salt, whether this is actually wise or not. Index of item, items value dot splice. Index self item. Okay. And then if we've got, and these actually probably don't need to be async functions. And then we probably got an async function for submit order. Alright. Submit order. Alright. This is gonna be constant data equals I don't know if that's gonna be data or not. We'll do await. We'll do use directus. Then we are going to create an item in the orders table or orders collection, the total for that order total, ordertotal will be the order total dot value. Then we have our items, which are going to be, should just be items. Alright. So we could do something like, nope. We're not gonna do that. We are going to do unref items since that is unrefed. We've got the store info and this customer field, inside direct us, I can save the current user on create. So let's do that. So the store is going to be where are we gonna get the store from? Maybe we're gonna pass that in the function. Store ID, that's a string. Store ID. Alright. Is this cool? We're gonna return these other functions from our composable. Let's see how we do. Alright. So now if we go back to our app, we've got the hamburger house. Let's just remove this, comment that out. Alright. So we got our menu, We want to push these items into a cart. Where are we at time wise? We got 10 minutes left. Are we gonna actually get these into a cart or not? So here inside this specific, page we might do what we are going to return. Let's take a look at our composable. Right. We've got our order items. We've got our items in the cart. Let's call the we're gonna get items. Let's call these cart items. Add to cart. Remove from cart equals used cart. K. And then can we hey. Let's just drop in our cart items here just to see what that looks like. Cart items. It's It's not showing anything for our card items. Are we getting an error? Let's actually take a look. So we'll go to ID. Cart items equals undefined. Oh, it's because I'm getting items. There we go. Alright. So we're showing just an array. Let's just do this. I'm just gonna give this some padding. BG gray, 50. Let's make it 100. This is our, p tag cart items. Okay. Alright. So now what we want to do on each one of these, right, if we click an add to cart button, which we we don't necessarily have. Where is that? Right? Why do we not have a button for add to cart? Maybe we don't even put that. That shouldn't even be in here basically. So let's move this up. We'll just take the class. We'll apply that to the card itself. Why is it not doing what we want? Gapspacey2. Why are you not doing what I want you to do? Alright. I'm not gonna stress over it. We'll just wrap it in a div because this will be the div task adventure. I know all the non tailwind CSS guys are probably freaking out at this moment, but okay. We'll use this u button component provided by Nuxt UI and we're going to click add to cart. We're going to add that item to the cart. Yeah. Close enough. Right. So if I click add to cart, we can see we've got our cart item here. It's not necessarily what I want. Right? The data structure is not entirely what I want. So let's actually just define it here. The item ID, we want this to be our order items. Alright. So we go into order items, We got the menu item. Let's do that. Menu item. Okay. Special instructions. No onions, please. Yeah. We'll just automatically populate that. The quantity, let's set that to 1. And then the item subtotal, we'll just say item price. Alright. So now if I refresh, does that give us kind of the structure that we want? It's still populating the entire menu item. We just want the menu item item.id. That should give us what we need. Okay. Does that match the right structure? Great. Where we at on time? We got, 5 minutes or so. Let's actually get this order together. Right? Alright. So inside our header, maybe we could display our cart items there. I really wanted to actually show the submitting to Directus. So knowing that I am short on time, right, we could go in and add these items. Let's just try submitting it right now and see. Alright. So we got our cart items. Let's go in. We've got new button, place order, and we'll call this submit order. Are we passing we're passing that store ID as well. Alright. So this isn't probably the best particular type of design, but it will get the job done. Let's do a large button. At click is submit order and then we've got the store dot ID that we're passing to this. And then let's go in and just like what does our submit order look like? This should be probably like try catch. So we could catch any type of errors. Response. If we catch an error, we want to return the response. Great. Submit order. Oh, that looks a little rough. Let's move that over here. Oh, I put I did I totally put that in the wrong spot anyway. Submit order and let's just hide this if the user is not logged in as well. Right? We don't want them to be able to place an order if you are not logged in. So this composable that I have, I want to say, has a or like this module that I set up has some of this already built in. So this will be v Where's the directus auth? Directus auth, user. So we'll do something like this where we get constant user equals use direct us off if there is a user. Okay. Alright. And if there's not a user, let's do a u button. V of no user, we are going to I think I could do this with it, where we have login to place an order. Login to order. And we will do what is it? 2 auth, I don't even need that. We do to auth/auth/login. Login to order. Okay. We've got our user. I didn't set up a user, but let's create one now. This will be our user. Test usertest@example.com. Let's give them a real secure password. We'll do password here and then I am going to select the role of user. Great. Save it. Now I should actually be able to log in. So test at example dot com, Password of that login. Tried to log in to the wrong spot. Where's the redirect for this? Use direct us off. Is this going to be in my Nuxt config? We want to redirect to auth login, not portal. We want to redirect to the index page. Oh, we're gonna we're gonna be cutting this one really really close. Come on. Alright. So we go back. Do we have we've got our menus. We don't have any cart orders. It looks like I'm still am I still logged in? Do we have the cookie? We've got okay. So we've got a session token here, so it's still showing me as logged in. If we hit add menu items, I hit place order, what happens is we can see the order posted. If we go inside Directus, do we have that specific order? Yes, we do. So we've got here's the store information, here's the order, we've got the item subtotal. We don't have the prices not being populated. So I didn't item subtotal should be item dot price. Why is that not populating correctly? I didn't add the price either. But item dot price. Cool. I'm calling this a win. So we've got what have we done? We've set up our data model within an hour. We've got all the items coming through. I could go in and clean this up, certainly, and that would be my next steps. But we've got a really simple UI here on the front end that with probably another 20, 30 minutes and a little bit of love. Man, this thing would be amazing. So I can go in, if I refresh this page, we should probably ditch that, but I can go in and add my pancakes. I can place the order and see it show up inside Directus. Alright. So we just do a quick review. What did we achieve? Right. We got the data model done, check. We got a list of the menu items for a restaurant, check. And then we got the place an order for menu items. Boom. Knock all 3 of those off. I'm gonna credit that to my wife being here to help me today. That's it for this episode, guys. I hope you've enjoyed this one. If you did, make sure you hit the subscribe button on Directus TV because we want you to catch more of this content. See you.",[347],"48bbe7ad-29b8-4ffd-aa87-c6924e0a75e4",[],{"id":172,"number":131,"show":122,"year":173,"episodes":350},[175,176,177,178,179,180,181,182,183,184,185],{"id":184,"slug":352,"vimeo_id":353,"description":354,"tile":355,"length":192,"resources":8,"people":356,"episode_number":358,"published":359,"title":360,"video_transcript_html":361,"video_transcript_text":362,"content":8,"seo":8,"status":130,"episode_people":363,"recommendations":365,"season":366},"airbnb","908297167","Can Bryant follow AirBnb's trajectory from scrappy startup to short term rental royalty – and build an AirBnb clone in one hour or less? Follow along as he attempts to builds a complete backend and frontend to manage rental listings, bookings, hosts, and more.","58537616-bf5c-4251-ae41-115fae3d13df",[357],{"name":199,"url":200},10,"2024-02-12","Mission: Airbnb","\u003Cp>Speaker 0: Alright. Alright. Alright. Welcome back to the next episode of 100 Apps, 100 Hours, where we build or rebuild some of your favorite apps, or publicly fail trying. I'm your host Brian Gillespie, developer advocate at Directus.\u003C/p>\u003Cp>And today we have the mother of all clones, Airbnb. This was actually going to be one of the first episodes that I was going to record for this series. To be honest, I got a little scared. Airbnb has a ton of functionality on the front end. We have authentication, we have listings, you have 2 different types of users, and that you have folks who are listing their properties, folks who are booking those properties, you have messages, you have reviews.\u003C/p>\u003Cp>There's a ton of things just underneath the surface. So I'm curious to see how far we can get in an hour. I don't want to bite off more than I could chew, but, should be entertaining nonetheless. Alright. Alright.\u003C/p>\u003Cp>So if you've tuned into the series before, you'll know we have two rules. The first is you have 60 minutes to plan and build, or I have 60 minutes to plan and build, if you've got 60 minutes, if you're watching along, maybe you want to build with me. It's great. No more, no less, that's all we get. And then the second rule is kind of the anti rule.\u003C/p>\u003Cp>Use whatever you have at your disposal. AI, chat GPT, I've got a Nuxt front end application starter set up that already has some plumbing to connect to my Directus Instance, and I have a blank Directus project. That's what I've got. We're gonna try to build this Airbnb clone. Let's get started.\u003C/p>\u003Cp>Alright, so we'll start the clock here and away we go, right? So I'm going to pull up the Airbnb website, if I can actually figure out how to work my computer, and let's just poke around and see what we can find, right? One of the first things that I usually like to do when I'm trying to break down functionality for an app is take a look at the navigation, right? Airbnb has this concept of stays versus experiences now, so we have properties or listings or something like that. We have experiences which are, tours and tastings and and actual things that you can do.\u003C/p>\u003Cp>So that should be an interesting dynamic. I I think we'll mostly probably just stick to the properties part of it, like the listings. You know, one of the other things that I always like to take a look at, you know, I can go in and check the network requests, you know, and and try to suss out what some of the the data model looks like for all these different things that we're querying on the front end. So it looks like there is a very, very interesting, set of queries they're using. If I look into, like, the actual property values, we've got this very detailed data that we're returning and using potentially for our layout.\u003C/p>\u003Cp>I don't really know how helpful this is going to be, right? And to my knowledge, I don't know that Airtable, or I'm sorry, Airbnb has a public API that I can use to kind of introspect the schema, so basically I'm just playing it by ear here, right? So on every episode, before I actually build, I like to do a bit of planning. And first, let's start with like what type of functionality we're going to try to achieve out of this. I certainly want to get the back end data model built for this, we'll map that out in just a moment.\u003C/p>\u003Cp>On the front end maybe we want to display a listing detail page and maybe a listing index page. Let's call that our original goal. If we got like a stretch goal, maybe we want to let folks book booking pages, booking properties, sending messages, something like that. So I think these three pieces here will be plenty to chew on for an hour. Let's, let's try to map this application out.\u003C/p>\u003Cp>And I really like Figma, FigJam, the combo is great for mapping out things like this. We'll just do no fill. Alright, so as far as our data model, naming things is always tricky. So are we gonna call this Listings, is it Properties? That's gonna be our main collection, right, this is our main table.\u003C/p>\u003Cp>Not a 100% sold on what it's gonna be yet. We've probably got users in the system, so we'll have our users and Directus is going to actually take care of that for us with the Directus users collection that that it bootstraps whenever we actually create our instance. What else do we have? We've got, reviews that are going to be included and it can be attached to the individual listings. Looks like we have categories as well.\u003C/p>\u003Cp>Right? Tiny Homes, Countryside, I don't know if those are gonna be tags. I guess you could be a Countryside and a Tiny Home. So that could be like a mini to mini relationship. So let's just call those tags.\u003C/p>\u003Cp>Great. We could tag those onto a property. We have bookings. Alright. So you've got a calendar of bookings for a property or listing.\u003C/p>\u003Cp>We can make those little relationships. Just draw some nice looking arrows, basically. That's all we're doing here. Then we've probably got a junction table for that mini to mini relationship. Cool.\u003C/p>\u003Cp>Just drag that out of the way. We've got our reviews. Tags. Okay. Listing tags.\u003C/p>\u003Cp>Alright. What else? If we open up filters, we could see things like, the type of place, the price range, bedrooms, number of bedrooms, property type, the amenities. That's kind of a nice bit of functionality. How are we going to deal with that?\u003C/p>\u003Cp>What are the booking options? Host language. If we go to the actual detail page for a property, right, we've got a gallery of multiple images, we've got a host that could be our user, I guess. You know, we might have a like a host profile or something like that, that we attach so that we don't pollute our users collection. We've got a schedule, we've got the reviews, we've got a map, yeah.\u003C/p>\u003Cp>Okay. I feel pretty good about this. Let's just draw a couple more arrows, just for the sake of arrows. Let's actually start building this out. Cool.\u003C/p>\u003Cp>Alright, so I'm going to drag this over and as you can see over here on the left I've got my blank instance of Directus. What are we gonna call this main one? Properties to me as a developer, I'm thinking like properties of an object or something like that, so I'm I'm gonna call this listings. If anybody from Airbnb is watching this, hey, maybe tell me what you're what kind of schema you're using on the back end. All right, so I'm going to create a new collection called Listings.\u003C/p>\u003Cp>We're going to use the generated UUID. Is this a published listing? Yes, probably. So we'll keep status, we'll have the created on, updated on, user created, etcetera. And let's go through and start modeling this out.\u003C/p>\u003Cp>Alright. So if I look, we've got a h one, this is probably just the title of the listing. Great. What happens to our URL? It looks like they don't use fancy URLs or like a slug for this listing.\u003C/p>\u003Cp>It's just actually by the ID. And it looks like it's rooms. I could have just looked at the URL. I kinda like listings better just because they are using, they're listing whole houses on here as well, right, not just a room. So, cool.\u003C/p>\u003Cp>We will have a, this looks like it's calculated. What are the other things that we need? The number of guests, the number of bedrooms, the number of bathrooms, right, these are all going to be inputs. We can support how many guests, let's say max guests. That sounds like a good one.\u003C/p>\u003Cp>One of the other nice things here is the ability inside Us, if you're using it for your content management, you can add these helpful notes for your users. So how many guests total can stay at this property. And I can also translate these into other languages, which is really nice for our international users. Alright. That looks good.\u003C/p>\u003Cp>This will always be an integer. We don't support half people here at Airbnb, that's what we'll call this one. So max guess, we have number of bedrooms. Right? I can just duplicate that particular field and say bedrooms quantity, bedroom number of bedrooms.\u003C/p>\u003Cp>Like bedrooms quantity. Sounds great. I may go in and edit this individual note though. How many total bedrooms? Cool.\u003C/p>\u003Cp>We'll go through and let's do bathrooms quantity. Again, and naming is probably, I won't say it's half the battle, but it is a lot of the battle in development, is coming up with names that make sense and have meaning. So we've got our title, we've got our max guest, we've got our bedrooms, we'll have a host for that, so we'll deal with that in a moment. I can see we've got a nice description here. This looks like it supports WYSIWYG content, so we'll go ahead and add the WYSIWYG editor within Directus.\u003C/p>\u003Cp>We'll call this the Description. Could be the about section. I see about the space here. We'll save this. Great.\u003C/p>\u003Cp>And then we have this what this place offers. Right? So this is an interesting one where we've got items that are not included, we've got items that are included. There's a couple different ways we could actually manage this within our data model. We could, I like hard code this as like JSON values or something like that, where we have a list of these objects.\u003C/p>\u003Cp>We could also set it up as a separate relationship so you could add these features, or I'm assuming what we would call features to this particular listing. Maybe come back to that one. Let's keep rolling down. We have our reviews, we have the address, so we could have the street address. Let's go ahead and fill that up.\u003C/p>\u003Cp>Street address, we do this as street address 1, Street address too, just in case we need it. What else do we have? City, region, state. I think it's region if we're being international friendly outside of the US. And then we have a postal code.\u003C/p>\u003Cp>Great. Alright. Maybe we clean this up a little bit. I'm gonna make this half width. When I'm inputting these things I want to make it look really nice.\u003C/p>\u003Cp>I am going to go in and use the detail group within Directus just to get a nice visual. So we'll say detail group, let's call this Address Info, on the front end, whenever we call the API, we would either make sure we omit this address so nobody can find this property without actually booking it. We want to keep that private. Or we may not even, send that to the front end at all. But obviously you want to store that.\u003C/p>\u003Cp>And we could, you know, even potentially like obfuscate this somehow. So we've got a host. We'll come back to that. House rules. Let's take a look at this.\u003C/p>\u003Cp>Pets allowed, no smoking. Check-in, check-in, check out. Yeah. So we're gonna need details on check-in, check out. We've got safety and properties.\u003C/p>\u003Cp>We've got safety devices. We've got a cancellation policy. Before you book, partial refund. Okay. Yeah, let's tackle just like safety really quickly.\u003C/p>\u003Cp>So we'll go in. This seems something like pretty simple. Let's go in and this may be a good use case for our checkboxes inside Directus. So safety, what do they call these? Safety and property.\u003C/p>\u003Cp>Let's just call it safety devices. We'll say carbon monoxide. Then we have a smoke alarm installed. Great. And for the value, I can even change this to something like CO 2 or CO.\u003C/p>\u003Cp>Right? Carbon monoxide is single c0. Yeah. Let's just leave it there. Great.\u003C/p>\u003Cp>I'm trying to think of what other safety devices we'd have on a property. Alright? Let's just check out one of these other cabins. Scroll down to the bottom. Potential for noise, so there is some road noise.\u003C/p>\u003Cp>This is kind of like a extra metadata as well. Safety devices. Let's just finish this out. Great. We could do something interesting here where like property info is just text and an explanation.\u003C/p>\u003Cp>Right? Maybe we even have a potential icon for this. So let's use the repeater field inside Directus. Basically it just stores as JSON data, but I can create an array of objects here that can contain basically almost anything that I want. But again, it just gets stored as JSON within the database.\u003C/p>\u003Cp>So we'll call this property info, and for the fields, it looks like we've got an icon. Right, icon. We'll call the field icon, we have a string, that's the type of the field, and then we actually have an icon interface inside Directus where you can choose lovely icons from a drop down. Alright, next we'll have a label or a title for this. Let's just go with Label, that'll be a string as well.\u003C/p>\u003Cp>We'll use just a standard input interface and what's next? Right, we've got a description, label, description, close enough. And this we'll just make a text box. I don't think we need WYSIWYG input here, we'll just do text area. I said we didn't need it and then I clicked it, got a little click happy there.\u003C/p>\u003Cp>Great. Okay, so now we've got safety devices, we've got property info, we could even call this, we could add this in our detail group as well. We could call it safety property. Safety and property. Great.\u003C/p>\u003Cp>And we can even add a little safety sign. Great. Health and safety. Beautiful. So I'll just drag those within this and we are starting to build a great looking data model for this.\u003C/p>\u003Cp>Alright. Let's cover check-in, check out info. So we can use the date time field. Do we want I guess we just want time. Right?\u003C/p>\u003Cp>Check-in time. We will let's do that half width. We'll do checkout time. Cool. We've got those 2 items.\u003C/p>\u003Cp>Was there any other details on check-in? Self check-in with keypad. Maybe there's like a check-in type. Let's take a look at a couple other listings and see what we can find. Where did you go?\u003C/p>\u003Cp>There we go. Self check-in with a lockbox. Yeah. So this is probably just like some text. Check out type.\u003C/p>\u003Cp>Great. Again, if I wanted to group these together, I could add a nice little detail group. Check out, let's call it House Rules, right? House Rules. Great.\u003C/p>\u003Cp>We'll put that under house rules, check out, check-in, check out type. Okay. If I could actually click and drag the way I want. Let's, let's move that up. We've got the address info, we've got the house rules, we've got the bedrooms, max guests, pets allowed.\u003C/p>\u003Cp>That could be a Boolean attribute or a toggle here. It just stores this Boolean in the database. So our pets allowed, Default. Let's say no is the default. Pets allowed as the label.\u003C/p>\u003Cp>Great, great. Alright, we'll shrink this to half width. I'm getting really anxious to see what this actual form looks like once we get done mocking out our data model here. We've got quiet hours, right? So how do we adjust for this?\u003C/p>\u003Cp>Do we have a range of dates inside Directus? We do not. Quiet, so we could do something like quiet time start. Date time. Oh, nope.\u003C/p>\u003Cp>We just want time. Date, time, and our dates. We're gonna have to delete that one. Alright. Let's just go back.\u003C/p>\u003Cp>Let's try again. We just want the time, right. We're not really concerned with the date. Quiet time, stop quiet quiet time start. Quiet time in.\u003C/p>\u003Cp>Great. Isn't data modeling fun, guys? We'll put that in our house rules. Quiet time. Does anybody allow smoking in their properties?\u003C/p>\u003Cp>Do we even really need to concern ourselves with this? Right? Additional rules. This doesn't look like it actually supports WYSIWYG content. So we can add a new field for that.\u003C/p>\u003Cp>We'll just call it additional rules. We'll just call it a text area. That's gonna store as text in the actual database. And one of the important things to know, hey, as we're going through and modeling this data out, if I were to pull up this SQL database right now, and I could show you that in just a moment, Directus is actually going through and mirroring every change that I'm making here inside the Data Studio, inside our actual database as well. So we go to house rules, we'll just clear that up.\u003C/p>\u003Cp>Too many, There we go. Alright. I need to adjust the sensitivity on the mouse. But if I open up table plus and just to give you guys a preview of what's going on behind the scenes here. Uh-oh.\u003C/p>\u003Cp>What am I doing wrong? Connect to the database. Detour. Let's check my Docker Compose. 8 055.\u003C/p>\u003Cp>Those ports should be exposed. Oh, I see what the problem is. I've got, another instance of PostgreSQL running through Docker. All right, so if I open this up, you can see I've got a listings table here. And if I look at the structure, you can see we've got those same columns that we were mapping inside Directus, and you can see the data type is being appropriately applied.\u003C/p>\u003Cp>Likewise, if I were to add a column to the database here in TablePlus or via SQL, Directus would introspect those changes and actually show up inside the app itself. So it is a beautiful mirroring relationship. It's, I love it. It's one of my favorite features of Directus. I I know I say that a lot.\u003C/p>\u003Cp>What other things do we need to offer here? HDTV, Mountain View, Valley View. I I I don't know what this looks like behind the scenes when you're actually mapping these out. I would be curious to see. Free washer, free dryer in unit versus free washer, free dryer out of unit, outside of unit.\u003C/p>\u003Cp>Right, we've got a garden view. I'm assuming this is probably just like some type of JSON data or something that's being stored, maybe free washer end unit. Let's go back. Can we actually filter based on this data? Can you filter based on okay.\u003C/p>\u003Cp>Yeah. So so you can filter on some of this. It doesn't look like you could filter on all of it. Like, hey, does it have a in unit versus a a non unit? I'm trying to think of the best way to account for these.\u003C/p>\u003Cp>I'm not sure exactly how I want to do it. Right? So we've got this type of place that we might set up as well. Let's do that. We're gonna do the type of place and then we have our property type.\u003C/p>\u003Cp>Let's just call this one type. Okay. We'll make this a string and instead of using an input, let's use radio buttons. I think we just have a room or an entire home. Alright.\u003C/p>\u003Cp>Destinations, guests. You could filter a room or entire home, basically. Yeah. That that works for me. Alright.\u003C/p>\u003Cp>So we'll say this is a room. And one of the other cool little things that I'll show you while we're here, I can translate this content inside Directus. If I use a dollar sign t and use this key and I hit enter, I can go in and add translation strings for any of the languages, which is really nice so that, whoever is using this, Directus instance, if they are editing content and they go in and they happen to be French or Canadian. You guys speak a different language up there in Canada. Right?\u003C/p>\u003Cp>No kidding. And then we'll call this the entire home. But this translation feature is really nice. Only recently started digging into it. So the value we'll call entire home.\u003C/p>\u003Cp>And then if I want to, what I could do is go into our translations here inside the Directus Admin. I can create a custom translation, and I could say entire home, pick my English language. Great. Oh, you do have different English up there in Canada. And I could say I could call this Bryant's home if I wanted to.\u003C/p>\u003Cp>Right? It doesn't really matter, but if I were to go in and look at this data model now, right, for the type I can see it's a room or I can see it's Bryant's home. So that's really nice in that I can apply these translations for any of the languages that our our team may speak. Entire home, great. What do they say here?\u003C/p>\u003Cp>Room in a home. Alright. So if we create a new one for room, we'll go English US, a room and a home. Great. Alright.\u003C/p>\u003Cp>So we go back, we create a listing, now we can see we've got 2 options there. We've got an entire home, we've got a room and a home. Cool. Let's, let's continue finishing this up. Price ranges could be interesting, right?\u003C/p>\u003Cp>I'm trying to think of of how we would set this particular data up, because I'm imagining this varies based on, like, is it a Monday night versus a Friday night or versus a Sunday night? How many days that you're staying? Can we actually Airbnb your home? Blah blah blah. Airbnb setup.\u003C/p>\u003Cp>I guess I could log in to my account. Don't look at my passwords. Let's see what it looks like when we actually Airbnb at home. Tell us about your place. Start on your own.\u003C/p>\u003Cp>This seems to be a nice little onboarding flow as well. My house is a castle. Right? Great. Entire place, room, a shared room.\u003C/p>\u003Cp>Okay. So that's one that we forgot as well. Right? A room, and then we have shared room. So again, we'll do that dollar sign t and a colon that allows us to store that as a translatable string.\u003C/p>\u003Cp>Great. This is gonna be the entire place. Enter my address. I'm not gonna do that here. Let's do, it was Yankee Stadium.\u003C/p>\u003Cp>Right? Let's let's do the address for Yankee Stadium. Bronx. Great. Confirm the address.\u003C/p>\u003Cp>Is this PIN in the right spot? Do they have any validation on this? I guess they don't. Right? So I wonder if this goes up to, what, I don't know how many people, Yankee Stadium seats, but we'll call it 16 plus.\u003C/p>\u003Cp>Right? So we've got the number of guests, bedrooms, beds. There's something else that we need to add here. Right? Bed quantity.\u003C/p>\u003Cp>Great. Should've done this to begin with. Right? Let's add another group because this is gonna get messy. A lot of fields on this one and, you know, again, this is why I didn't want to bite off too much on this actual one.\u003C/p>\u003Cp>So we'll call this the, what do we call this? Basics listing info. Yeah. That's fine. We won't we won't get too carried away here.\u003C/p>\u003Cp>So we got the type, we got the listing info, just move that up. Max number of guests, bedrooms quantity, bathrooms quantity, are pets allowed. We'll hoist title and description up here. Let's say we've got 8 beds, 8 bedrooms, maybe 5 bedrooms. Again, no idea how many of those are inside Yankee Stadium.\u003C/p>\u003Cp>Make your place stand out. Okay. Let's look behind the scenes and see, is this making any calls here? We have parking, just using React. So I wonder Okay.\u003C/p>\u003Cp>If we amenities. Amenities. Can we see any of those here? I don't see them. I I I really don't wanna dig through each of these components here to try to figure that out.\u003C/p>\u003Cp>Can you actually do, like, the view thing in the React Dev Tools? I guess you can. Alright. So what's our consumer, provider, Value, History. Yeah.\u003C/p>\u003Cp>I'm not a not a React guy. Let's just not even bother diving through it. Right? There's our safety items, like first aid kits, smoke alarm, etcetera. Standout amenities.\u003C/p>\u003Cp>So these are just the basics, right? And you might even go through and add separate fields for for these things. So we could do again the detail group for this. Maybe this one starts closed. Call this amenities.\u003C/p>\u003Cp>These also could be like a separate database table, that we we add, and that way we could continue adding those. Yes, no, the other. Again, not knowing behind the scenes the importance of this, like maybe we add these as separate things so we could filter them. So let's go in and we'll add a new collection. We'll call it Amenities.\u003C/p>\u003Cp>Amenities. Probably need some spell checking there. Amenities. Cool. Alright.\u003C/p>\u003Cp>All of this, I'm not really concerned myself with. We have an icon for the amenity. Gotta have an icon. Very design oriented here myself. Recovering designer, as I like to refer to myself.\u003C/p>\u003Cp>We have a name for this amenity. Maybe we've got something like a key for it as well. I I I don't know. And then is there some type of explanation for these? Yeah.\u003C/p>\u003Cp>You may have something like a description for it as well. Cool. Good enough. Now what we're gonna do is link these 2 together, right? So inside Directus what I can do is basically create a many to many relationship, because one property could have many amenities and many amenities could be linked to many properties.\u003C/p>\u003Cp>I think that's a great explanation of amenities and many to many relationships. Directus makes this super simple to create these relationships. So on our listings collection, we'll just call this Amin I need to work on my spelling for sure. My wife works in the school system as well, so maybe she can give me some help. We don't wanna allow duplicates.\u003C/p>\u003Cp>We wanna to show a link to that specific item. If I open up advanced field creation mode I can actually control the name of this junction collection. That's going to create a junction table in our SQL database. I'm okay with this, right? And if I wanted to I could add a reverse many to many relationship, on the amenities collection back to the listings.\u003C/p>\u003Cp>What we may do is add a sort field here as well. For the display, we'll show related values. We can come back in and clean that up in just a moment. But, great, we've got our amenities here. Let's move that up to the top.\u003C/p>\u003Cp>What are the other things that we need to add? Some photos of the castle. You need 5 photos to get started. I'm feeling pretty confident here. I'm just going to close this out.\u003C/p>\u003Cp>Exit. We'll go back to the Airbnb website. And there was one thing that I forgot what I was gonna add here. Is, property type. That's what it was.\u003C/p>\u003Cp>So we'll go in, I'm just gonna duplicate this one called type and we'll add property type. Duplicate, that's of course gonna drop down here to the bottom and I'll just bring it back up. Property type, let's just clear out these values. So it probably didn't need to duplicate that. But we have a house.\u003C/p>\u003Cp>I'm not gonna mess with the translation strings here. Just know that's available to you if you are working with Directus. We'll call this an apartment. Apartment. Guest house.\u003C/p>\u003Cp>For those of you who do have a guest house, amazing. Take advantage of it. Exploit it on Airbnb. We have the hotel and that looks pretty good. Cool.\u003C/p>\u003Cp>At some point we'll probably have to deal with the pricing thing, but for now we'll just skip it. Alright, so I'm feeling pretty confident about this. Let's go in and flesh out the other items in our data model. We spend a ton of time data modeling this out, but there's a lot of fields on the listings. So we got reviews, we have tags.\u003C/p>\u003Cp>So let's create reviews. What does the review structure look like inside Airbnb? So we're certainly gonna want a user created. We'll probably have a status on the review. Hey, is this published or not?\u003C/p>\u003Cp>For the reviews, okay. It looks like we have an overall rating. We have cleanliness, accuracy, location, value. Alright. So I think we have a slider field that it could be a good representation for this.\u003C/p>\u003Cp>Let's call it overall rating. Great. We'll duplicate that one. We can call it cleanliness. Cleanliness?\u003C/p>\u003Cp>Yeah, I think so. Looks great. Let's duplicate this. We'll have the accuracy and so forth. I just missed that one, didn't I?\u003C/p>\u003Cp>Duplicate. Alright. So you kind of get the picture there. We have a user, we have some actual text. It doesn't look like, to me that doesn't look like WYSIWYG text.\u003C/p>\u003Cp>Not that it matters. The WYSIWYG and the text area interface both store the data as text inside the database. So we'll call this, what, Content? This is our message. And I'm not sure if this is like other platforms where you get a response as well, where if somebody leaves you a crappy review, you can respond to it.\u003C/p>\u003Cp>So we got reviews. Let's link those to our listings. Alright. Again, that's gonna be a many to many relationship. So we'll say Reviews, we'll do Reviews, all this is gonna be happening for us.\u003C/p>\u003Cp>One thing that I see a lot of people miss is this sort field on the relationship page, so you can actually sort these items in the order that you want. Probably not necessary for reviews because you're just going to, do those probably based on recency or or the actual date. What else do we have? In our original model, we had host profile, we had our reviews, we had our tags. Let's go in and add tags.\u003C/p>\u003Cp>Great. Not not really concerned with any of this. We'll just call it a tag name. Okay. And again, we're going to be using our many to many relationship.\u003C/p>\u003Cp>Because we could have many tags on many different listings. Sweet. Great. Anything else that we need? Let's do a host or hosts as we'll call them, or we could call this host profiles maybe, because they're they're we're gonna link these back to the Directus users collection.\u003C/p>\u003Cp>Again, just so we're not gumming up or you know, making a mess out of the system table. But behind the scenes, if I pull back up my database, you'll see that all of your SQL data here, all your data in your SQL database, it remains purer because all of the metadata that Directus uses, it stores in its own name space tables. The one that kind of sits in the middle of that is Directus Users. This is what you get out of the box. You get the authentication, we have the permissions that are tied to this, so you get rule based access control, which is really nice.\u003C/p>\u003Cp>Right? You get all of this out of the box. I hate to junk up my system tables, though. So what I usually do in situations like this is just add a link back to those. So instead of a mini to mini, we'll just do like a mini to 1.\u003C/p>\u003Cp>And we'll call this our user. For the related collection it's gonna be directus underscore users. And I'm gonna go into field advanced creation mode, we'll call this host profile. And one other thing I'm gonna do is make this value unique. Great.\u003C/p>\u003Cp>Okay. So now we have our host profile. What does this look like? Santa is our host. Do we have any other details?\u003C/p>\u003Cp>Let's pull somebody else up. I'll click the same one. See what kind of data we're storing on the host profile. Alright. Probably an image on the host profile.\u003C/p>\u003Cp>Great. So we'll just add an image, or we could call this Avatar. And how are we doing on time? Looks like we've got roughly like 15 minutes left. We're doing a lot of data modeling on this.\u003C/p>\u003Cp>We've got our user avatar. We've got a description. Great. Alright. Let's go in and we've got the details for that.\u003C/p>\u003Cp>Let's add our host to the listing as well. So a listing belongs to a single host. I see we've got co hosts here, but, not super concerned with that. That's gonna be our many to one collection, or relationship. So a host owns a single property or a host is responsible for a single property, but a host could have many properties.\u003C/p>\u003Cp>So we'll choose our host profile. Great. Save it. Okay. Alright, so let's go in and look at this giant form that we're gonna add all of our data.\u003C/p>\u003Cp>Right, now the really cool part here is I've modeled all this data out, and I'll show you in a moment. I'm just gonna copy and paste basically everything from this. If I was smart I probably would have built like a scraper or something to input all this data. Cool. There we go.\u003C/p>\u003Cp>We've got the description when it comes time to the amenities. Let's just go in and create a new amenity. We've got mountains, view. Yeah, that kinda looks like a mountain view. Mountain view.\u003C/p>\u003Cp>We could give it a key. That could be helpful if we're actually filtering this on the front end. Dedicated workspace. Alright. So I can go through and do a desk icon.\u003C/p>\u003Cp>Not quite as fancy as what they have here. We'll just use Workspace as the key. Cool. Great. So we added some amenities.\u003C/p>\u003Cp>This is what? This is a entire cabin? Yeah. This looks like a house. This is an entire home.\u003C/p>\u003Cp>We have up to 4 guests. You can see we've got that nice little note that we had here, so this is just providing that extra little for our user experience. We've got 1 bedroom, 2 beds, 1 bath. Are pets allowed? Do we see pets on this one?\u003C/p>\u003Cp>Let's say no. Right? Check-in time, where do we find that? We've got our check-in time is 4 pm. 4 PM.\u003C/p>\u003Cp>Check out time, 10 AM. K. Check out type. Self check-in with a lockbox. Do we have a quiet time?\u003C/p>\u003Cp>I don't see quiet time. I'm gonna set quiet time. Right? From midnight to 5 AM? I don't remember.\u003C/p>\u003Cp>Great. Additional rules, we can see outside guests, no allowed parties, lots of additional rules at this particular place. We've got the address, I'm not even going to bother filling that out. We'll add some property info. What do we have there?\u003C/p>\u003Cp>Must Climb Stairs. That's a great one, right? Must climb stairs. You have to be able to climb some stairs to access the property. Stairs.\u003C/p>\u003Cp>We even have a nice stairs icon. Great. We don't have any reviews. We're gonna tag this as a cabin in the woods. And then we have our host.\u003C/p>\u003Cp>Right? So we'll just create a new host profile. Who is our host? Sierra. I I'm not gonna do that.\u003C/p>\u003Cp>Let's just use, like, user avatar, sample user avatar. Yeah. This guy looks great. Kinda like Tim Cook or something. Copy image address, upload, we'll call this guy test host, and save.\u003C/p>\u003Cp>Alright. So now we've got a listing. The only thing we're actually missing here is some images. Right? So I can go back into our listing, I can hit create, and here we can add files to this.\u003C/p>\u003Cp>And files could be images, we're just gonna call it images, that's great. We can even choose what folder we want to upload these things to. So we'll save that, drag it back to the top, add some images for this, and where'd you go? I'm just gonna pick one of these. One of the things that I really like is, it's super handy to be able to upload images from a URL here without having to download them.\u003C/p>\u003Cp>Not sure about you, but I usually get like a 1,000,000 1,000,000 files on my desktop that, you know, just clutter up things after a while. Okay. Alright. So now we've created this listing, right? And, if I pull up our application, we got this really nice Nuxt application.\u003C/p>\u003Cp>I'm gonna just move Airbnb out of my way as well. But if I pull up this application Oh, I closed Directus. Let me bring that back. I still haven't mastered Arc. I'm not sure if you guys are Arc users or not, but, it can be a challenge.\u003C/p>\u003Cp>Alright. So we've got Directus, we've got our local host. I'm gonna pull up our code base. This is just a senior Nuxt application. I've already got an SDK configured for this.\u003C/p>\u003Cp>And if I look, I've got an extra leftover page here, but we've got this index page, we've got 100 apps, 100 hours. If I wanted to just use like the async data call from NUXT. So if I look at the NUXT documentation, they have a composable in here, use async Data for Data Fetching. And this takes care of things like caching, refreshing, it gives you like pending state, things like that that you can use. So I could potentially just copy this here.\u003C/p>\u003Cp>We've got Use Async Data. Let's call this Listings, so we give it a key. This could be like the actual route, like the full path of the route. But here, I'm going to go in and we are going to do something like this, where we say return use directus. I've got a composable here and I'm going to read items from the listings collection.\u003C/p>\u003Cp>The next thing I would add here could be, you see Copilot is already offering some suggestions of hey, only show me published listings or like the specific fields that I want. Let's not even bother with that at the moment. I may just leave that on there, though. Great. Do we have everything we need?\u003C/p>\u003Cp>Refresh interval. Where am I missing? I don't need refresh interval. Don't really want refresh interval. What does our parser say?\u003C/p>\u003Cp>Do I have too many of these? Let's just keep it simple. Oh, yeah, missing the last one. But there's that's what I was missing. Okay.\u003C/p>\u003Cp>So now I'm just gonna log this out. So if I do data, maybe we wrap it in a pre tag just to see what that looks like. Are we getting anything back from Directus? No. Why not?\u003C/p>\u003Cp>Subscripts. Parse error. Use async data listings. We're going to return use directus read items. That should be what I've got set up here.\u003C/p>\u003Cp>Use directus. Use directus rest. No. It should just be oh. Do we need to return a wait?\u003C/p>\u003Cp>Nope. What is the problemo? Let's actually just ditch async data for a moment. Const data equals use await. Use directus, read items listings.\u003C/p>\u003Cp>Alright. And this should do the fetching here. And now I can see the problem. Nuxt has SSR configured out of the gate, so I wasn't seeing those errors here on the client side. But basically this is showing 403 forbidden.\u003C/p>\u003Cp>And the reason why is because I haven't set up my permissions inside Directus. So if I were to just log that back, I could go into Directus and under our rule based access control, I'll just go in and for now, and because we're running out of time here anyway, I'm just gonna set this to public. All this data that we set up for the individual listings, all of our tags. One of the things that I could do here, right, this is public data, so I don't want to maybe I want to show the city and like the state or the country, but I can go in and I can set custom permission settings. So where I have the, you know, I could go in and say okay, only items that are published will be shown and are available to the public, so without login.\u003C/p>\u003Cp>But things like street address and postal code, you know, if I was storing data like phone numbers or email addresses, right, I wouldn't want to make that data available to the public. You know, the rest of this data, hey, they need to know and we probably still want to surface that if they are, just browsing the site and they're not logged in. But for the actual sensitive data, I don't want to I don't want to give that up. Alright. We've saved our Let's just take a look and see what we get back here.\u003C/p>\u003Cp>Okay. So I can see the listings. Right? We're not getting any data. And again, I thought I was using async data wrong here.\u003C/p>\u003Cp>I need to brush up on its use, but the status of this post is not published. So now if I go in and publish it, because we set up that rule based access control, Now we can see we got some data. It's rendering kind of funky because I'm flexing it. Okay. Alright.\u003C/p>\u003Cp>So now we can see we're actually fetching all the data from our API. So which is really nice. Directus gives us these REST APIs. As soon as we set up our data model, we can see all this information inside here. All right.\u003C/p>\u003Cp>So if I look at the clock, we've got, just a few moments left. Let's go in and just try to flesh something out as far as a detail page. If I look, I've got some images here. How do I actually get access to this relational data? One of the nice things inside Directus is the ability to pass a fields array that will actually act as a GraphQL API.\u003C/p>\u003Cp>So you get all the benefits of GraphQL, and then I can specify all the fields that I want to return, and I can even grab relational data, as deep as I want to go, which you have to be very careful about recursive relationships. But within that, I can go and grab all of that. So if I wanted to grab the root level fields, I could. I could also do something like this where I say images.asterisk, that will give me the Directus Files ID that I would need to render these types on, on the actual on the site. Yeah.\u003C/p>\u003Cp>Simple enough. Alright. Let's flush this out as quickly as possible. Alright. So I'm gonna clean that up.\u003C/p>\u003Cp>What do we have here? Underlying this, I've been using the Nuxt UI library, but I'm always a huge fan of Tailwind UI. If you don't have it, sign up for it. It's a great tool. All right.\u003C/p>\u003Cp>This is probably, could be similar to a product page, maybe. Alright. Let's take a look. Product overviews. Got a couple of these.\u003C/p>\u003Cp>I really like this image grid component. Image gallery. Let's just lift this wholesale. Oh, the Linter is doing a number on this thing, right? So here we go back.\u003C/p>\u003Cp>We're probably getting an error somewhere. The View Dev Tools and the Nuxt Dev Tools, I can't recommend enough. They're very great. But if I go in and I look for my index page, I can see our data that we've got here. And maybe here, I actually want to, is there where's the key?\u003C/p>\u003Cp>I think I actually do something like this where I transform and I do data, and I do data dot 0. Is that valid? No. Yeah. Maybe that's right.\u003C/p>\u003Cp>No overload. Not just this call Because we've got the return. Oh, no. I think it needs to be one more out here. Okay.\u003C/p>\u003Cp>Now if I look, data transform. Product dot images. Okay. So this is gonna be data dot images. So if we just clean these up.\u003C/p>\u003Cp>Data data dot images. Oh, missed one. We'll just clean that up. Data dot images dot source. Yeah.\u003C/p>\u003Cp>So we're not getting source here. Are we getting any actual data at all? Right? It's not showing me anything in the view dev tools. This could be a case of where I spent way too much time data modeling, without actually building anything.\u003C/p>\u003Cp>But at least, you will have a good idea of the back end structure. 0 of undefined. Let's just, comment this out. Get our data back. Data.\u003C/p>\u003Cp>Pre. Alright. So we have data dot images. Now I gotta do data dot 1. Let's just do this.\u003C/p>\u003Cp>Our listing equals data, data dot value, and we'll log our listing. Okay. And now I should be able to do something like this where, I've got Nuxt Image connected to this as well, which is a great tool. So we use Nuxt Image and in the actual config for Nuxt you can set up the provider for Directus because we use the same underlying library. Where are you?\u003C/p>\u003Cp>And then I can set the base URL for my assets. So then I don't even have to actually worry about passing the URL. So Nuxt data dot images. Is it gonna be data? It's listing dot images, and then where it says source, we're actually gonna grab this directus file's ID.\u003C/p>\u003Cp>Underscore files underscore ID. Do we actually get images here? If I remove the alt text, does this actually work? Am I are are we kidding, man? Why do I not have this?\u003C/p>\u003Cp>Right? If I close this out, why is it not showing? Images, directus files underscore ID. Yeah. Alright, well regardless we hit the timer.\u003C/p>\u003Cp>I didn't get very far on the front end on this one. I chalk it up to spending too much time on the back end, but oh, what a back end it is. It's very beautiful. We've got a detailed data model that, we've got our REST API for, if you prefer GraphQL, you could query this on the front end to your heart's desire. Again, maybe I was rightfully a little scared of this Airbnb just because of everything that is involved in all the details on this.\u003C/p>\u003Cp>Anyway, I had a lot of fun with this one. I hope you enjoyed it. Stay tuned for the next episode. Got a great one coming. I'll see you.\u003C/p>","Alright. Alright. Alright. Welcome back to the next episode of 100 Apps, 100 Hours, where we build or rebuild some of your favorite apps, or publicly fail trying. I'm your host Brian Gillespie, developer advocate at Directus. And today we have the mother of all clones, Airbnb. This was actually going to be one of the first episodes that I was going to record for this series. To be honest, I got a little scared. Airbnb has a ton of functionality on the front end. We have authentication, we have listings, you have 2 different types of users, and that you have folks who are listing their properties, folks who are booking those properties, you have messages, you have reviews. There's a ton of things just underneath the surface. So I'm curious to see how far we can get in an hour. I don't want to bite off more than I could chew, but, should be entertaining nonetheless. Alright. Alright. So if you've tuned into the series before, you'll know we have two rules. The first is you have 60 minutes to plan and build, or I have 60 minutes to plan and build, if you've got 60 minutes, if you're watching along, maybe you want to build with me. It's great. No more, no less, that's all we get. And then the second rule is kind of the anti rule. Use whatever you have at your disposal. AI, chat GPT, I've got a Nuxt front end application starter set up that already has some plumbing to connect to my Directus Instance, and I have a blank Directus project. That's what I've got. We're gonna try to build this Airbnb clone. Let's get started. Alright, so we'll start the clock here and away we go, right? So I'm going to pull up the Airbnb website, if I can actually figure out how to work my computer, and let's just poke around and see what we can find, right? One of the first things that I usually like to do when I'm trying to break down functionality for an app is take a look at the navigation, right? Airbnb has this concept of stays versus experiences now, so we have properties or listings or something like that. We have experiences which are, tours and tastings and and actual things that you can do. So that should be an interesting dynamic. I I think we'll mostly probably just stick to the properties part of it, like the listings. You know, one of the other things that I always like to take a look at, you know, I can go in and check the network requests, you know, and and try to suss out what some of the the data model looks like for all these different things that we're querying on the front end. So it looks like there is a very, very interesting, set of queries they're using. If I look into, like, the actual property values, we've got this very detailed data that we're returning and using potentially for our layout. I don't really know how helpful this is going to be, right? And to my knowledge, I don't know that Airtable, or I'm sorry, Airbnb has a public API that I can use to kind of introspect the schema, so basically I'm just playing it by ear here, right? So on every episode, before I actually build, I like to do a bit of planning. And first, let's start with like what type of functionality we're going to try to achieve out of this. I certainly want to get the back end data model built for this, we'll map that out in just a moment. On the front end maybe we want to display a listing detail page and maybe a listing index page. Let's call that our original goal. If we got like a stretch goal, maybe we want to let folks book booking pages, booking properties, sending messages, something like that. So I think these three pieces here will be plenty to chew on for an hour. Let's, let's try to map this application out. And I really like Figma, FigJam, the combo is great for mapping out things like this. We'll just do no fill. Alright, so as far as our data model, naming things is always tricky. So are we gonna call this Listings, is it Properties? That's gonna be our main collection, right, this is our main table. Not a 100% sold on what it's gonna be yet. We've probably got users in the system, so we'll have our users and Directus is going to actually take care of that for us with the Directus users collection that that it bootstraps whenever we actually create our instance. What else do we have? We've got, reviews that are going to be included and it can be attached to the individual listings. Looks like we have categories as well. Right? Tiny Homes, Countryside, I don't know if those are gonna be tags. I guess you could be a Countryside and a Tiny Home. So that could be like a mini to mini relationship. So let's just call those tags. Great. We could tag those onto a property. We have bookings. Alright. So you've got a calendar of bookings for a property or listing. We can make those little relationships. Just draw some nice looking arrows, basically. That's all we're doing here. Then we've probably got a junction table for that mini to mini relationship. Cool. Just drag that out of the way. We've got our reviews. Tags. Okay. Listing tags. Alright. What else? If we open up filters, we could see things like, the type of place, the price range, bedrooms, number of bedrooms, property type, the amenities. That's kind of a nice bit of functionality. How are we going to deal with that? What are the booking options? Host language. If we go to the actual detail page for a property, right, we've got a gallery of multiple images, we've got a host that could be our user, I guess. You know, we might have a like a host profile or something like that, that we attach so that we don't pollute our users collection. We've got a schedule, we've got the reviews, we've got a map, yeah. Okay. I feel pretty good about this. Let's just draw a couple more arrows, just for the sake of arrows. Let's actually start building this out. Cool. Alright, so I'm going to drag this over and as you can see over here on the left I've got my blank instance of Directus. What are we gonna call this main one? Properties to me as a developer, I'm thinking like properties of an object or something like that, so I'm I'm gonna call this listings. If anybody from Airbnb is watching this, hey, maybe tell me what you're what kind of schema you're using on the back end. All right, so I'm going to create a new collection called Listings. We're going to use the generated UUID. Is this a published listing? Yes, probably. So we'll keep status, we'll have the created on, updated on, user created, etcetera. And let's go through and start modeling this out. Alright. So if I look, we've got a h one, this is probably just the title of the listing. Great. What happens to our URL? It looks like they don't use fancy URLs or like a slug for this listing. It's just actually by the ID. And it looks like it's rooms. I could have just looked at the URL. I kinda like listings better just because they are using, they're listing whole houses on here as well, right, not just a room. So, cool. We will have a, this looks like it's calculated. What are the other things that we need? The number of guests, the number of bedrooms, the number of bathrooms, right, these are all going to be inputs. We can support how many guests, let's say max guests. That sounds like a good one. One of the other nice things here is the ability inside Us, if you're using it for your content management, you can add these helpful notes for your users. So how many guests total can stay at this property. And I can also translate these into other languages, which is really nice for our international users. Alright. That looks good. This will always be an integer. We don't support half people here at Airbnb, that's what we'll call this one. So max guess, we have number of bedrooms. Right? I can just duplicate that particular field and say bedrooms quantity, bedroom number of bedrooms. Like bedrooms quantity. Sounds great. I may go in and edit this individual note though. How many total bedrooms? Cool. We'll go through and let's do bathrooms quantity. Again, and naming is probably, I won't say it's half the battle, but it is a lot of the battle in development, is coming up with names that make sense and have meaning. So we've got our title, we've got our max guest, we've got our bedrooms, we'll have a host for that, so we'll deal with that in a moment. I can see we've got a nice description here. This looks like it supports WYSIWYG content, so we'll go ahead and add the WYSIWYG editor within Directus. We'll call this the Description. Could be the about section. I see about the space here. We'll save this. Great. And then we have this what this place offers. Right? So this is an interesting one where we've got items that are not included, we've got items that are included. There's a couple different ways we could actually manage this within our data model. We could, I like hard code this as like JSON values or something like that, where we have a list of these objects. We could also set it up as a separate relationship so you could add these features, or I'm assuming what we would call features to this particular listing. Maybe come back to that one. Let's keep rolling down. We have our reviews, we have the address, so we could have the street address. Let's go ahead and fill that up. Street address, we do this as street address 1, Street address too, just in case we need it. What else do we have? City, region, state. I think it's region if we're being international friendly outside of the US. And then we have a postal code. Great. Alright. Maybe we clean this up a little bit. I'm gonna make this half width. When I'm inputting these things I want to make it look really nice. I am going to go in and use the detail group within Directus just to get a nice visual. So we'll say detail group, let's call this Address Info, on the front end, whenever we call the API, we would either make sure we omit this address so nobody can find this property without actually booking it. We want to keep that private. Or we may not even, send that to the front end at all. But obviously you want to store that. And we could, you know, even potentially like obfuscate this somehow. So we've got a host. We'll come back to that. House rules. Let's take a look at this. Pets allowed, no smoking. Check-in, check-in, check out. Yeah. So we're gonna need details on check-in, check out. We've got safety and properties. We've got safety devices. We've got a cancellation policy. Before you book, partial refund. Okay. Yeah, let's tackle just like safety really quickly. So we'll go in. This seems something like pretty simple. Let's go in and this may be a good use case for our checkboxes inside Directus. So safety, what do they call these? Safety and property. Let's just call it safety devices. We'll say carbon monoxide. Then we have a smoke alarm installed. Great. And for the value, I can even change this to something like CO 2 or CO. Right? Carbon monoxide is single c0. Yeah. Let's just leave it there. Great. I'm trying to think of what other safety devices we'd have on a property. Alright? Let's just check out one of these other cabins. Scroll down to the bottom. Potential for noise, so there is some road noise. This is kind of like a extra metadata as well. Safety devices. Let's just finish this out. Great. We could do something interesting here where like property info is just text and an explanation. Right? Maybe we even have a potential icon for this. So let's use the repeater field inside Directus. Basically it just stores as JSON data, but I can create an array of objects here that can contain basically almost anything that I want. But again, it just gets stored as JSON within the database. So we'll call this property info, and for the fields, it looks like we've got an icon. Right, icon. We'll call the field icon, we have a string, that's the type of the field, and then we actually have an icon interface inside Directus where you can choose lovely icons from a drop down. Alright, next we'll have a label or a title for this. Let's just go with Label, that'll be a string as well. We'll use just a standard input interface and what's next? Right, we've got a description, label, description, close enough. And this we'll just make a text box. I don't think we need WYSIWYG input here, we'll just do text area. I said we didn't need it and then I clicked it, got a little click happy there. Great. Okay, so now we've got safety devices, we've got property info, we could even call this, we could add this in our detail group as well. We could call it safety property. Safety and property. Great. And we can even add a little safety sign. Great. Health and safety. Beautiful. So I'll just drag those within this and we are starting to build a great looking data model for this. Alright. Let's cover check-in, check out info. So we can use the date time field. Do we want I guess we just want time. Right? Check-in time. We will let's do that half width. We'll do checkout time. Cool. We've got those 2 items. Was there any other details on check-in? Self check-in with keypad. Maybe there's like a check-in type. Let's take a look at a couple other listings and see what we can find. Where did you go? There we go. Self check-in with a lockbox. Yeah. So this is probably just like some text. Check out type. Great. Again, if I wanted to group these together, I could add a nice little detail group. Check out, let's call it House Rules, right? House Rules. Great. We'll put that under house rules, check out, check-in, check out type. Okay. If I could actually click and drag the way I want. Let's, let's move that up. We've got the address info, we've got the house rules, we've got the bedrooms, max guests, pets allowed. That could be a Boolean attribute or a toggle here. It just stores this Boolean in the database. So our pets allowed, Default. Let's say no is the default. Pets allowed as the label. Great, great. Alright, we'll shrink this to half width. I'm getting really anxious to see what this actual form looks like once we get done mocking out our data model here. We've got quiet hours, right? So how do we adjust for this? Do we have a range of dates inside Directus? We do not. Quiet, so we could do something like quiet time start. Date time. Oh, nope. We just want time. Date, time, and our dates. We're gonna have to delete that one. Alright. Let's just go back. Let's try again. We just want the time, right. We're not really concerned with the date. Quiet time, stop quiet quiet time start. Quiet time in. Great. Isn't data modeling fun, guys? We'll put that in our house rules. Quiet time. Does anybody allow smoking in their properties? Do we even really need to concern ourselves with this? Right? Additional rules. This doesn't look like it actually supports WYSIWYG content. So we can add a new field for that. We'll just call it additional rules. We'll just call it a text area. That's gonna store as text in the actual database. And one of the important things to know, hey, as we're going through and modeling this data out, if I were to pull up this SQL database right now, and I could show you that in just a moment, Directus is actually going through and mirroring every change that I'm making here inside the Data Studio, inside our actual database as well. So we go to house rules, we'll just clear that up. Too many, There we go. Alright. I need to adjust the sensitivity on the mouse. But if I open up table plus and just to give you guys a preview of what's going on behind the scenes here. Uh-oh. What am I doing wrong? Connect to the database. Detour. Let's check my Docker Compose. 8 055. Those ports should be exposed. Oh, I see what the problem is. I've got, another instance of PostgreSQL running through Docker. All right, so if I open this up, you can see I've got a listings table here. And if I look at the structure, you can see we've got those same columns that we were mapping inside Directus, and you can see the data type is being appropriately applied. Likewise, if I were to add a column to the database here in TablePlus or via SQL, Directus would introspect those changes and actually show up inside the app itself. So it is a beautiful mirroring relationship. It's, I love it. It's one of my favorite features of Directus. I I know I say that a lot. What other things do we need to offer here? HDTV, Mountain View, Valley View. I I I don't know what this looks like behind the scenes when you're actually mapping these out. I would be curious to see. Free washer, free dryer in unit versus free washer, free dryer out of unit, outside of unit. Right, we've got a garden view. I'm assuming this is probably just like some type of JSON data or something that's being stored, maybe free washer end unit. Let's go back. Can we actually filter based on this data? Can you filter based on okay. Yeah. So so you can filter on some of this. It doesn't look like you could filter on all of it. Like, hey, does it have a in unit versus a a non unit? I'm trying to think of the best way to account for these. I'm not sure exactly how I want to do it. Right? So we've got this type of place that we might set up as well. Let's do that. We're gonna do the type of place and then we have our property type. Let's just call this one type. Okay. We'll make this a string and instead of using an input, let's use radio buttons. I think we just have a room or an entire home. Alright. Destinations, guests. You could filter a room or entire home, basically. Yeah. That that works for me. Alright. So we'll say this is a room. And one of the other cool little things that I'll show you while we're here, I can translate this content inside Directus. If I use a dollar sign t and use this key and I hit enter, I can go in and add translation strings for any of the languages, which is really nice so that, whoever is using this, Directus instance, if they are editing content and they go in and they happen to be French or Canadian. You guys speak a different language up there in Canada. Right? No kidding. And then we'll call this the entire home. But this translation feature is really nice. Only recently started digging into it. So the value we'll call entire home. And then if I want to, what I could do is go into our translations here inside the Directus Admin. I can create a custom translation, and I could say entire home, pick my English language. Great. Oh, you do have different English up there in Canada. And I could say I could call this Bryant's home if I wanted to. Right? It doesn't really matter, but if I were to go in and look at this data model now, right, for the type I can see it's a room or I can see it's Bryant's home. So that's really nice in that I can apply these translations for any of the languages that our our team may speak. Entire home, great. What do they say here? Room in a home. Alright. So if we create a new one for room, we'll go English US, a room and a home. Great. Alright. So we go back, we create a listing, now we can see we've got 2 options there. We've got an entire home, we've got a room and a home. Cool. Let's, let's continue finishing this up. Price ranges could be interesting, right? I'm trying to think of of how we would set this particular data up, because I'm imagining this varies based on, like, is it a Monday night versus a Friday night or versus a Sunday night? How many days that you're staying? Can we actually Airbnb your home? Blah blah blah. Airbnb setup. I guess I could log in to my account. Don't look at my passwords. Let's see what it looks like when we actually Airbnb at home. Tell us about your place. Start on your own. This seems to be a nice little onboarding flow as well. My house is a castle. Right? Great. Entire place, room, a shared room. Okay. So that's one that we forgot as well. Right? A room, and then we have shared room. So again, we'll do that dollar sign t and a colon that allows us to store that as a translatable string. Great. This is gonna be the entire place. Enter my address. I'm not gonna do that here. Let's do, it was Yankee Stadium. Right? Let's let's do the address for Yankee Stadium. Bronx. Great. Confirm the address. Is this PIN in the right spot? Do they have any validation on this? I guess they don't. Right? So I wonder if this goes up to, what, I don't know how many people, Yankee Stadium seats, but we'll call it 16 plus. Right? So we've got the number of guests, bedrooms, beds. There's something else that we need to add here. Right? Bed quantity. Great. Should've done this to begin with. Right? Let's add another group because this is gonna get messy. A lot of fields on this one and, you know, again, this is why I didn't want to bite off too much on this actual one. So we'll call this the, what do we call this? Basics listing info. Yeah. That's fine. We won't we won't get too carried away here. So we got the type, we got the listing info, just move that up. Max number of guests, bedrooms quantity, bathrooms quantity, are pets allowed. We'll hoist title and description up here. Let's say we've got 8 beds, 8 bedrooms, maybe 5 bedrooms. Again, no idea how many of those are inside Yankee Stadium. Make your place stand out. Okay. Let's look behind the scenes and see, is this making any calls here? We have parking, just using React. So I wonder Okay. If we amenities. Amenities. Can we see any of those here? I don't see them. I I I really don't wanna dig through each of these components here to try to figure that out. Can you actually do, like, the view thing in the React Dev Tools? I guess you can. Alright. So what's our consumer, provider, Value, History. Yeah. I'm not a not a React guy. Let's just not even bother diving through it. Right? There's our safety items, like first aid kits, smoke alarm, etcetera. Standout amenities. So these are just the basics, right? And you might even go through and add separate fields for for these things. So we could do again the detail group for this. Maybe this one starts closed. Call this amenities. These also could be like a separate database table, that we we add, and that way we could continue adding those. Yes, no, the other. Again, not knowing behind the scenes the importance of this, like maybe we add these as separate things so we could filter them. So let's go in and we'll add a new collection. We'll call it Amenities. Amenities. Probably need some spell checking there. Amenities. Cool. Alright. All of this, I'm not really concerned myself with. We have an icon for the amenity. Gotta have an icon. Very design oriented here myself. Recovering designer, as I like to refer to myself. We have a name for this amenity. Maybe we've got something like a key for it as well. I I I don't know. And then is there some type of explanation for these? Yeah. You may have something like a description for it as well. Cool. Good enough. Now what we're gonna do is link these 2 together, right? So inside Directus what I can do is basically create a many to many relationship, because one property could have many amenities and many amenities could be linked to many properties. I think that's a great explanation of amenities and many to many relationships. Directus makes this super simple to create these relationships. So on our listings collection, we'll just call this Amin I need to work on my spelling for sure. My wife works in the school system as well, so maybe she can give me some help. We don't wanna allow duplicates. We wanna to show a link to that specific item. If I open up advanced field creation mode I can actually control the name of this junction collection. That's going to create a junction table in our SQL database. I'm okay with this, right? And if I wanted to I could add a reverse many to many relationship, on the amenities collection back to the listings. What we may do is add a sort field here as well. For the display, we'll show related values. We can come back in and clean that up in just a moment. But, great, we've got our amenities here. Let's move that up to the top. What are the other things that we need to add? Some photos of the castle. You need 5 photos to get started. I'm feeling pretty confident here. I'm just going to close this out. Exit. We'll go back to the Airbnb website. And there was one thing that I forgot what I was gonna add here. Is, property type. That's what it was. So we'll go in, I'm just gonna duplicate this one called type and we'll add property type. Duplicate, that's of course gonna drop down here to the bottom and I'll just bring it back up. Property type, let's just clear out these values. So it probably didn't need to duplicate that. But we have a house. I'm not gonna mess with the translation strings here. Just know that's available to you if you are working with Directus. We'll call this an apartment. Apartment. Guest house. For those of you who do have a guest house, amazing. Take advantage of it. Exploit it on Airbnb. We have the hotel and that looks pretty good. Cool. At some point we'll probably have to deal with the pricing thing, but for now we'll just skip it. Alright, so I'm feeling pretty confident about this. Let's go in and flesh out the other items in our data model. We spend a ton of time data modeling this out, but there's a lot of fields on the listings. So we got reviews, we have tags. So let's create reviews. What does the review structure look like inside Airbnb? So we're certainly gonna want a user created. We'll probably have a status on the review. Hey, is this published or not? For the reviews, okay. It looks like we have an overall rating. We have cleanliness, accuracy, location, value. Alright. So I think we have a slider field that it could be a good representation for this. Let's call it overall rating. Great. We'll duplicate that one. We can call it cleanliness. Cleanliness? Yeah, I think so. Looks great. Let's duplicate this. We'll have the accuracy and so forth. I just missed that one, didn't I? Duplicate. Alright. So you kind of get the picture there. We have a user, we have some actual text. It doesn't look like, to me that doesn't look like WYSIWYG text. Not that it matters. The WYSIWYG and the text area interface both store the data as text inside the database. So we'll call this, what, Content? This is our message. And I'm not sure if this is like other platforms where you get a response as well, where if somebody leaves you a crappy review, you can respond to it. So we got reviews. Let's link those to our listings. Alright. Again, that's gonna be a many to many relationship. So we'll say Reviews, we'll do Reviews, all this is gonna be happening for us. One thing that I see a lot of people miss is this sort field on the relationship page, so you can actually sort these items in the order that you want. Probably not necessary for reviews because you're just going to, do those probably based on recency or or the actual date. What else do we have? In our original model, we had host profile, we had our reviews, we had our tags. Let's go in and add tags. Great. Not not really concerned with any of this. We'll just call it a tag name. Okay. And again, we're going to be using our many to many relationship. Because we could have many tags on many different listings. Sweet. Great. Anything else that we need? Let's do a host or hosts as we'll call them, or we could call this host profiles maybe, because they're they're we're gonna link these back to the Directus users collection. Again, just so we're not gumming up or you know, making a mess out of the system table. But behind the scenes, if I pull back up my database, you'll see that all of your SQL data here, all your data in your SQL database, it remains purer because all of the metadata that Directus uses, it stores in its own name space tables. The one that kind of sits in the middle of that is Directus Users. This is what you get out of the box. You get the authentication, we have the permissions that are tied to this, so you get rule based access control, which is really nice. Right? You get all of this out of the box. I hate to junk up my system tables, though. So what I usually do in situations like this is just add a link back to those. So instead of a mini to mini, we'll just do like a mini to 1. And we'll call this our user. For the related collection it's gonna be directus underscore users. And I'm gonna go into field advanced creation mode, we'll call this host profile. And one other thing I'm gonna do is make this value unique. Great. Okay. So now we have our host profile. What does this look like? Santa is our host. Do we have any other details? Let's pull somebody else up. I'll click the same one. See what kind of data we're storing on the host profile. Alright. Probably an image on the host profile. Great. So we'll just add an image, or we could call this Avatar. And how are we doing on time? Looks like we've got roughly like 15 minutes left. We're doing a lot of data modeling on this. We've got our user avatar. We've got a description. Great. Alright. Let's go in and we've got the details for that. Let's add our host to the listing as well. So a listing belongs to a single host. I see we've got co hosts here, but, not super concerned with that. That's gonna be our many to one collection, or relationship. So a host owns a single property or a host is responsible for a single property, but a host could have many properties. So we'll choose our host profile. Great. Save it. Okay. Alright, so let's go in and look at this giant form that we're gonna add all of our data. Right, now the really cool part here is I've modeled all this data out, and I'll show you in a moment. I'm just gonna copy and paste basically everything from this. If I was smart I probably would have built like a scraper or something to input all this data. Cool. There we go. We've got the description when it comes time to the amenities. Let's just go in and create a new amenity. We've got mountains, view. Yeah, that kinda looks like a mountain view. Mountain view. We could give it a key. That could be helpful if we're actually filtering this on the front end. Dedicated workspace. Alright. So I can go through and do a desk icon. Not quite as fancy as what they have here. We'll just use Workspace as the key. Cool. Great. So we added some amenities. This is what? This is a entire cabin? Yeah. This looks like a house. This is an entire home. We have up to 4 guests. You can see we've got that nice little note that we had here, so this is just providing that extra little for our user experience. We've got 1 bedroom, 2 beds, 1 bath. Are pets allowed? Do we see pets on this one? Let's say no. Right? Check-in time, where do we find that? We've got our check-in time is 4 pm. 4 PM. Check out time, 10 AM. K. Check out type. Self check-in with a lockbox. Do we have a quiet time? I don't see quiet time. I'm gonna set quiet time. Right? From midnight to 5 AM? I don't remember. Great. Additional rules, we can see outside guests, no allowed parties, lots of additional rules at this particular place. We've got the address, I'm not even going to bother filling that out. We'll add some property info. What do we have there? Must Climb Stairs. That's a great one, right? Must climb stairs. You have to be able to climb some stairs to access the property. Stairs. We even have a nice stairs icon. Great. We don't have any reviews. We're gonna tag this as a cabin in the woods. And then we have our host. Right? So we'll just create a new host profile. Who is our host? Sierra. I I'm not gonna do that. Let's just use, like, user avatar, sample user avatar. Yeah. This guy looks great. Kinda like Tim Cook or something. Copy image address, upload, we'll call this guy test host, and save. Alright. So now we've got a listing. The only thing we're actually missing here is some images. Right? So I can go back into our listing, I can hit create, and here we can add files to this. And files could be images, we're just gonna call it images, that's great. We can even choose what folder we want to upload these things to. So we'll save that, drag it back to the top, add some images for this, and where'd you go? I'm just gonna pick one of these. One of the things that I really like is, it's super handy to be able to upload images from a URL here without having to download them. Not sure about you, but I usually get like a 1,000,000 1,000,000 files on my desktop that, you know, just clutter up things after a while. Okay. Alright. So now we've created this listing, right? And, if I pull up our application, we got this really nice Nuxt application. I'm gonna just move Airbnb out of my way as well. But if I pull up this application Oh, I closed Directus. Let me bring that back. I still haven't mastered Arc. I'm not sure if you guys are Arc users or not, but, it can be a challenge. Alright. So we've got Directus, we've got our local host. I'm gonna pull up our code base. This is just a senior Nuxt application. I've already got an SDK configured for this. And if I look, I've got an extra leftover page here, but we've got this index page, we've got 100 apps, 100 hours. If I wanted to just use like the async data call from NUXT. So if I look at the NUXT documentation, they have a composable in here, use async Data for Data Fetching. And this takes care of things like caching, refreshing, it gives you like pending state, things like that that you can use. So I could potentially just copy this here. We've got Use Async Data. Let's call this Listings, so we give it a key. This could be like the actual route, like the full path of the route. But here, I'm going to go in and we are going to do something like this, where we say return use directus. I've got a composable here and I'm going to read items from the listings collection. The next thing I would add here could be, you see Copilot is already offering some suggestions of hey, only show me published listings or like the specific fields that I want. Let's not even bother with that at the moment. I may just leave that on there, though. Great. Do we have everything we need? Refresh interval. Where am I missing? I don't need refresh interval. Don't really want refresh interval. What does our parser say? Do I have too many of these? Let's just keep it simple. Oh, yeah, missing the last one. But there's that's what I was missing. Okay. So now I'm just gonna log this out. So if I do data, maybe we wrap it in a pre tag just to see what that looks like. Are we getting anything back from Directus? No. Why not? Subscripts. Parse error. Use async data listings. We're going to return use directus read items. That should be what I've got set up here. Use directus. Use directus rest. No. It should just be oh. Do we need to return a wait? Nope. What is the problemo? Let's actually just ditch async data for a moment. Const data equals use await. Use directus, read items listings. Alright. And this should do the fetching here. And now I can see the problem. Nuxt has SSR configured out of the gate, so I wasn't seeing those errors here on the client side. But basically this is showing 403 forbidden. And the reason why is because I haven't set up my permissions inside Directus. So if I were to just log that back, I could go into Directus and under our rule based access control, I'll just go in and for now, and because we're running out of time here anyway, I'm just gonna set this to public. All this data that we set up for the individual listings, all of our tags. One of the things that I could do here, right, this is public data, so I don't want to maybe I want to show the city and like the state or the country, but I can go in and I can set custom permission settings. So where I have the, you know, I could go in and say okay, only items that are published will be shown and are available to the public, so without login. But things like street address and postal code, you know, if I was storing data like phone numbers or email addresses, right, I wouldn't want to make that data available to the public. You know, the rest of this data, hey, they need to know and we probably still want to surface that if they are, just browsing the site and they're not logged in. But for the actual sensitive data, I don't want to I don't want to give that up. Alright. We've saved our Let's just take a look and see what we get back here. Okay. So I can see the listings. Right? We're not getting any data. And again, I thought I was using async data wrong here. I need to brush up on its use, but the status of this post is not published. So now if I go in and publish it, because we set up that rule based access control, Now we can see we got some data. It's rendering kind of funky because I'm flexing it. Okay. Alright. So now we can see we're actually fetching all the data from our API. So which is really nice. Directus gives us these REST APIs. As soon as we set up our data model, we can see all this information inside here. All right. So if I look at the clock, we've got, just a few moments left. Let's go in and just try to flesh something out as far as a detail page. If I look, I've got some images here. How do I actually get access to this relational data? One of the nice things inside Directus is the ability to pass a fields array that will actually act as a GraphQL API. So you get all the benefits of GraphQL, and then I can specify all the fields that I want to return, and I can even grab relational data, as deep as I want to go, which you have to be very careful about recursive relationships. But within that, I can go and grab all of that. So if I wanted to grab the root level fields, I could. I could also do something like this where I say images.asterisk, that will give me the Directus Files ID that I would need to render these types on, on the actual on the site. Yeah. Simple enough. Alright. Let's flush this out as quickly as possible. Alright. So I'm gonna clean that up. What do we have here? Underlying this, I've been using the Nuxt UI library, but I'm always a huge fan of Tailwind UI. If you don't have it, sign up for it. It's a great tool. All right. This is probably, could be similar to a product page, maybe. Alright. Let's take a look. Product overviews. Got a couple of these. I really like this image grid component. Image gallery. Let's just lift this wholesale. Oh, the Linter is doing a number on this thing, right? So here we go back. We're probably getting an error somewhere. The View Dev Tools and the Nuxt Dev Tools, I can't recommend enough. They're very great. But if I go in and I look for my index page, I can see our data that we've got here. And maybe here, I actually want to, is there where's the key? I think I actually do something like this where I transform and I do data, and I do data dot 0. Is that valid? No. Yeah. Maybe that's right. No overload. Not just this call Because we've got the return. Oh, no. I think it needs to be one more out here. Okay. Now if I look, data transform. Product dot images. Okay. So this is gonna be data dot images. So if we just clean these up. Data data dot images. Oh, missed one. We'll just clean that up. Data dot images dot source. Yeah. So we're not getting source here. Are we getting any actual data at all? Right? It's not showing me anything in the view dev tools. This could be a case of where I spent way too much time data modeling, without actually building anything. But at least, you will have a good idea of the back end structure. 0 of undefined. Let's just, comment this out. Get our data back. Data. Pre. Alright. So we have data dot images. Now I gotta do data dot 1. Let's just do this. Our listing equals data, data dot value, and we'll log our listing. Okay. And now I should be able to do something like this where, I've got Nuxt Image connected to this as well, which is a great tool. So we use Nuxt Image and in the actual config for Nuxt you can set up the provider for Directus because we use the same underlying library. Where are you? And then I can set the base URL for my assets. So then I don't even have to actually worry about passing the URL. So Nuxt data dot images. Is it gonna be data? It's listing dot images, and then where it says source, we're actually gonna grab this directus file's ID. Underscore files underscore ID. Do we actually get images here? If I remove the alt text, does this actually work? Am I are are we kidding, man? Why do I not have this? Right? If I close this out, why is it not showing? Images, directus files underscore ID. Yeah. Alright, well regardless we hit the timer. I didn't get very far on the front end on this one. I chalk it up to spending too much time on the back end, but oh, what a back end it is. It's very beautiful. We've got a detailed data model that, we've got our REST API for, if you prefer GraphQL, you could query this on the front end to your heart's desire. Again, maybe I was rightfully a little scared of this Airbnb just because of everything that is involved in all the details on this. Anyway, I had a lot of fun with this one. I hope you enjoyed it. Stay tuned for the next episode. Got a great one coming. I'll see you.",[364],"e53dfaa1-451f-454f-a737-fe10430ecdf2",[],{"id":172,"number":131,"show":122,"year":173,"episodes":367},[175,176,177,178,179,180,181,182,183,184,185],{"id":185,"slug":369,"vimeo_id":370,"description":371,"tile":372,"length":285,"resources":8,"people":373,"episode_number":384,"published":385,"title":386,"video_transcript_html":387,"video_transcript_text":388,"content":8,"seo":8,"status":130,"episode_people":389,"recommendations":394,"season":395},"slack","918839097","In this special edition live episode, Bryant is joined by Kevin, Alex, and Matt to build a clone on Slack in just one hour with Directus Realtime and Nuxt. Join the absolute chaos as friends and colleagues try to \"help\" against the clock. ","08d6a2f3-8c06-4d00-b7c0-cec62b2144cb",[374,375,378,381],{"name":199,"url":200},{"name":376,"url":377},"Alex van der Valk","https://directus.io/team/alex-van-der-valk",{"name":379,"url":380},"Kevin Lewis","https://directus.io/team/kevin-lewis",{"name":382,"url":383},"Matt Minor","https://directus.io/team/matt-minor",11,"2024-03-06","Mission: Slack (Live Episode)","\u003Cp>Speaker 0: Build in an extra 15 minutes for chaos. I'm gonna be here for the first 20. Then for the 1st presales engineers, Alex, will be here for 20, and then Matt will be here for 20. With that in mind, Bryant, I'm gonna start a timer. No stress, but stress.\u003C/p>\u003Cp>Take it away.\u003C/p>\u003Cp>Speaker 1: Yeah. Let's start the timer. What are we building?\u003C/p>\u003Cp>Speaker 0: You tell me.\u003C/p>\u003Cp>Speaker 1: Yeah. So you you let me know ahead of time, maybe, like, just a hour ago that we were gonna be building. Any guesses in the chat. Right? This might be fun to do live.\u003C/p>\u003Cp>Right? What are the guesses? No guesses. Alright. I don't see him coming through.\u003C/p>\u003Cp>We are going to be building a Slack clone. Build direct to us.\u003C/p>\u003Cp>Speaker 0: In an hour.\u003C/p>\u003Cp>Speaker 1: No. In an hour. Alright. So, Slack. Right?\u003C/p>\u003Cp>Yeah. Everybody uses it. You either love it, hate it. Xbox Live. Yeah.\u003C/p>\u003Cp>We'll do Xbox Live next time. Alright. So let's sketch this thing out. Right? What do we need out of a Slack clone?\u003C/p>\u003Cp>What feels good as far as functionality for this? How are we gonna set it up in direct us? Facebook, they need some help with their APIs. Yep. Certainly.\u003C/p>\u003Cp>Yeah. Experienced\u003C/p>\u003Cp>Speaker 0: that. We all saw that today. What is needed for Slack? I suppose the lightest version is gonna be you have channels, and inside of channels, you have users, and then you have channels. And then inside the channels, you have messages with, like, real time.\u003C/p>\u003Cp>And I think the that's the slimmest version. Right?\u003C/p>\u003Cp>Speaker 1: Yeah. So let's put, like, channels and messages at the top. We would call, like, threads a stretch goal. Yeah. Thread threading feels like, probably more than an hour.\u003C/p>\u003Cp>Alright. So we got channels. We got messages. We're gonna get the user authentication from direct to its users. And as far as functionality, what do we wanna do?\u003C/p>\u003Cp>We wanna have a channel. We want to submit messages in the channel. We want that all to update in real time. Notifications? Do we want notifications?\u003C/p>\u003Cp>We'll\u003C/p>\u003Cp>Speaker 0: I never wanna be notified when people send me messages ever. There is literally no world where I want that feature. So\u003C/p>\u003Cp>Speaker 1: so how many Slack groups are you a part of where, like, you just got the whole thing muted, Kevin? That would be a good question.\u003C/p>\u003Cp>Speaker 0: So let's take a look here. Let me let me open Slack for the first time in a in a hot minute. Okay. I'm in 123456789101112 Slacks. 12 Slacks.\u003C/p>\u003Cp>And I look at 2 of them regularly. 3 of them regularly. Most of the others are muted.\u003C/p>\u003Cp>Speaker 1: Alright. Yep. That's how it goes. Same thing for me. Alright.\u003C/p>\u003Cp>So I've got a brand new instance. Please don't use this password.\u003C/p>\u003Cp>Speaker 0: Oh my lord. I okay. That's fine. Everything's fine.\u003C/p>\u003Cp>Speaker 1: Probably need to change that before we get some surprises. Right? Change that real quick. Okay. Alright.\u003C/p>\u003Cp>So we're inside Directus. How are we gonna map this out? Right? We get the Directus users already out of the box. We get the authentication.\u003C/p>\u003Cp>We should get real time out of the box as well. So we just need these components. Right? Channels and messages should be fairly straightforward. We'll just call it a channel.\u003C/p>\u003Cp>Speaker 0: Why would you say that? That's like the beginning of the it should be it should be straightforward. Keep it simple, and it won't be complicated.\u003C/p>\u003Cp>Speaker 1: Keep it simple, and it won't be complicated. Yeah. Alright. What do we need for a channel? Do we yeah.\u003C/p>\u003Cp>Yeah. We don't even really need any of this. Right? I don't care when it was created, who it was created by. Just need a name for the channel.\u003C/p>\u003Cp>That'll be an input. We're going to go to the advanced settings. And because I want this to be URL safe, right, we'll use the slugify option inside the interface, and boom. There's channels. That was really complicated.\u003C/p>\u003Cp>I I saw Ben was\u003C/p>\u003Cp>Speaker 0: He begins now.\u003C/p>\u003Cp>Speaker 1: Let's let's do, like, the sahash. Oh, egg.\u003C/p>\u003Cp>Speaker 0: Oh, god. We don't have time for this, Brian. Dude God, I'm stressed already, and we're, like, 4 minutes in. Everything.\u003C/p>\u003Cp>Speaker 1: Yep. Alright. So then we've got messages. Alright. So as far as messages, we want when that message was created.\u003C/p>\u003Cp>So let's just call that timestamp for simplicity. Fun fact.\u003C/p>\u003Cp>Speaker 0: I didn't know you could rename those. I literally didn't that is the first time I've ever seen that. Damn.\u003C/p>\u003Cp>Speaker 1: Yeah. You could change it. You know, like, some people prefer updated at, versus date created or update updated. Do whatever you want with it. Alright.\u003C/p>\u003Cp>So we've got some messages within the messages. What do we have? We have what do we wanna do? Just text text area. We could go to markdown to be super complicated, but we'll just call it content, text, message.\u003C/p>\u003Cp>What do you Yeah.\u003C/p>\u003Cp>Speaker 0: I literally I literally don't care. Oh my god. No. I I wouldn't do message because if you're, like, looping through messages, you might use message as the singular, and then, yeah, text feels good.\u003C/p>\u003Cp>Speaker 1: Text. Alright. Let's unhide this just so we could see.\u003C/p>\u003Cp>Speaker 0: My leg's literally doing its nervous shake. I'm so glad this is your series and not mine. Oh my gosh. I'm sweating. Is it is it\u003C/p>\u003Cp>Speaker 1: just the technical difficulties at the start of it? Or or what?\u003C/p>\u003Cp>Speaker 0: No. It's having an hour on the clock. It's having an hour on the clock, Brian. It's the very format.\u003C/p>\u003Cp>Speaker 1: Yeah. No worries. Alright. So we got some messages. We got some channels.\u003C/p>\u003Cp>What else do we really need here? Oh, we'll come back to threads later. Right? We've already got our users. I'm gonna go ahead and, let's add you as a user, Kevin.\u003C/p>\u003Cp>Kevin, add example. And I'm not gonna tell you guys what the password is here. Alright. Now let's go into\u003C/p>\u003Cp>Speaker 0: I did actually think reactions would be a nice sorry. That was me interacting with, Ben in the chat. I did actually think reactions would be a nice a nice one as well. It's quite common.\u003C/p>\u003Cp>Speaker 1: Yeah. Cool. It'd be an interesting one to add. Yeah. We'll come back to it for sure.\u003C/p>\u003Cp>Alright. So we're gonna create a role for users so we can give access to messages for channels, etcetera. We're just gonna go ahead and give all access to this to start with. And the other thing that I'm gonna do, let's give public access to create we'll come back to, like, registering users.\u003C/p>\u003Cp>Speaker 0: Yeah. Yeah. Yeah. No. That's later.\u003C/p>\u003Cp>Speaker 1: Yeah. Okay. Alright. So we've given access. Now we can access any as long as we're in this user role, we'll be able to access channels, messages, all of that.\u003C/p>\u003Cp>Let's actually start to build something. Right? If I wanted to, I could give Kevin access to this right now. He could log in. Let's just do that.\u003C/p>\u003Cp>Speaker 0: You're just seeing my stress face here. Okay.\u003C/p>\u003Cp>Speaker 1: I just see your stress face. Kevin at example.\u003C/p>\u003Cp>Speaker 0: And Kevin Oh, that is a lowbrow password.\u003C/p>\u003Cp>Speaker 1: Is it lowbrow?\u003C/p>\u003Cp>Speaker 0: Wow. I mean Yeah.\u003C/p>\u003Cp>Speaker 1: So let's go in. I'm gonna just create a couple\u003C/p>\u003Cp>Speaker 2: of channels.\u003C/p>\u003Cp>Speaker 0: Tests, though.\u003C/p>\u003Cp>Speaker 1: Oh, yeah. That's right. Let me just go ahead and give that. Because eventually, you're gonna be interacting through this Nuxt front end. But Yeah.\u003C/p>\u003Cp>Yeah. Cool. Alright. So we create a couple channels. Right?\u003C/p>\u003Cp>What do we like, guys? Throw some suggestions in the chat. We'll add a few of these channels. Let's start with Random? Evan is crazy nervous channel.\u003C/p>\u003Cp>Speaker 0: My leg's shaking.\u003C/p>\u003Cp>Speaker 1: General, memes. Okay. Sounds great. Got some memes. Alright.\u003C/p>\u003Cp>And now as far as messages, right, we got the user. We got the timestamp. We've got when it was updated. In case we change it, we got the text. But the thing that we don't have is how these messages belong to a channel.\u003C/p>\u003Cp>So inside Directus, we'll just go in. Again, creating these relationships, super easy. I don't even have to touch SQL. We'll go in, create a mini to 1 relationship for this. We'll call it channel, and we'll use our channels collection.\u003C/p>\u003Cp>And\u003C/p>\u003Cp>Speaker 2: what else do we need?\u003C/p>\u003Cp>Speaker 1: Maybe display template. We'll use name. Great. Cool. And now I could populate a message into a specific channel.\u003C/p>\u003Cp>Hey, Kev. I'll be nervous.\u003C/p>\u003Cp>Speaker 0: I have the most faith in you while having absolutely no faith in you at the same time. By the way, that user you created for me still doesn't have app access. You might need to go into the user settings and allow that. I thought doing the role would be sufficient, but, oh, I'm not I'm not in that role. I'm not in that role.\u003C/p>\u003Cp>Users in that role was 0.\u003C/p>\u003Cp>Speaker 1: Yeah. Gotcha. Yep. Try it now. Okay.\u003C/p>\u003Cp>Speaker 0: I'll tell you in a moment.\u003C/p>\u003Cp>Speaker 1: Tell me in a moment. Alright. So we've got the basic logic here. Like, we could go and fetch the data from this API if we wanted to just by going to messages or item slash messages. We're gonna get a forbidden because I'm not logged in, but that's okay.\u003C/p>\u003Cp>We'll take care of that.\u003C/p>\u003Cp>Speaker 0: Oh, we're 10 minutes in. This? Sorry. No stress, but we're 10 minutes in. That's half my time here.\u003C/p>\u003Cp>I'm I'm halfway to freedom because I am sweating so hard.\u003C/p>\u003Cp>Speaker 1: Don't sweat, man. Somehow I got logged out. So chaos rains, my friend. Oh my gosh. Glass work.\u003C/p>\u003Cp>Okay. Alright. I don't know how I got logged out, but okay. So we've got channels. We've got messages.\u003C/p>\u003Cp>Let's actually do something. I low key hate this, he says. I can still see the chat in the other window, Kevin. So you're not you're not giving me much confidence.\u003C/p>\u003Cp>Speaker 0: I'm so sorry.\u003C/p>\u003Cp>Speaker 1: Alright. Yeah. No worries, man. No worries. Alright.\u003C/p>\u003Cp>So this is my NUC starter from all the other episodes that we've got. I've switched it up a little bit just so you have a simple plug in for the sake of this live instance because the the other starter that had had a Nuxt module. It's got a lot going on. This is really simple. Right?\u003C/p>\u003Cp>So we've got a direct us plug in, that is in the plug ins directory inside Nuxt, which will automatically register this. And really simple. Right? Inside Nuxt, we've got a runtime config that we're calling to get the directus URL. And my Nuxt config looks like this.\u003C/p>\u003Cp>Right? We've got a direct us URL. Right now, that is pointing to, like, a local host, so we do need to switch this over. We'll just go to our URL here. It is switched over.\u003C/p>\u003Cp>Great. As far as the server token, I don't think we even actually will be using this. I don't think I've got it anywhere else in my config either. Cool. So we'll save that.\u003C/p>\u003Cp>And now we want to, like, start fetching some data. Right? There's a couple of routes that I already have set up in this, like a login and a register route. Drink a red bar too.\u003C/p>\u003Cp>Speaker 0: I've got all my caffeine.\u003C/p>\u003Cp>Speaker 1: More I've\u003C/p>\u003Cp>Speaker 0: got my caffeine right here. Don't you worry.\u003C/p>\u003Cp>Speaker 1: Give him some more energy drinks, please.\u003C/p>\u003Cp>Speaker 0: I'm gonna go get one. I'll be right back. Woof. Woof. I am actually doing it.\u003C/p>\u003Cp>Speaker 1: Alright. So let's fire up the dev server for Nux here. As far as the stuff that I've already got, like I said, I've got a a login route, a register route that we'll try to mess with. Looks like this. We'll go in and log in, where we can register.\u003C/p>\u003Cp>Very simple. And then we just have a index page, and then, I had some testing that I was doing here. Alright. So as far as layout, what do we want? You know, a lot of times, I it's like I I love Tailwind.\u003C/p>\u003Cp>Tailwind UI is totally worth the money, especially for, like, quick prototypes like this. I usually like, if it's just something I'm hacking on personally, I'll start with, like, the application shells. Like, sidebar layout. Right? This feels pretty good for, like, a Slack type of setup.\u003C/p>\u003Cp>Speaker 0: Yeah. That looks good.\u003C/p>\u003Cp>Speaker 1: Yep. So let's just go in. We've got that set to view. Where are we gonna run into issues at? Probably with the icons because I don't have those set, but no matter.\u003C/p>\u003Cp>Alright. So as far as our pages, let's do what are we gonna do here? Let's just do, like, an app directory. Within that, we'll probably have, like, some channels.\u003C/p>\u003Cp>Speaker 0: Yeah. That makes sense. Just just And then the dynamic route inside of that. Yeah.\u003C/p>\u003Cp>Speaker 1: Yep. So we'll do, like, channel dot view. And then also, what I can do is set up, like, a a parent root for the app. So if I go here inside the pages directory and I do app dot view, I paste this in.\u003C/p>\u003Cp>Speaker 0: Is this for, like, the sidebar? Like, the sidebar will be held in this?\u003C/p>\u003Cp>Speaker 1: Yeah. Exactly. Nice. Alright. So we'll save that.\u003C/p>\u003Cp>Let's see what we get.\u003C/p>\u003Cp>Speaker 0: That is a major indentation. Yikes.\u003C/p>\u003Cp>Speaker 1: K. So these are Rike's rules. I'm not sure if he's on here, but, alright.\u003C/p>\u003Cp>Speaker 0: He quit out. He hit the wrong keyboard shortcut. That is absolutely what happened. He got arced. That's what our team say when you accidentally, like, quit the whole window.\u003C/p>\u003Cp>I'm sure he'll be back in a moment. Until then, you got me, stressed as heck. Got my energy drink, waiting for the waiting for the stress to return. This is my brief moment of respite here. That was a major indentation, right?\u003C/p>\u003Cp>Is it worth the time of pause? Is it worth the time of pause?\u003C/p>\u003Cp>Speaker 1: Did you get arced? I I don't know what happened. Yeah. I guess I did.\u003C/p>\u003Cp>Speaker 0: We're back. Sorry. You don't need to be sorry and you don't get that time back.\u003C/p>\u003Cp>Speaker 1: Oh, yeah. Yeah. It's still running. I don't know what happened, man. I really don't.\u003C/p>\u003Cp>Speaker 0: Weird.\u003C/p>\u003Cp>Speaker 1: All right. So getting some errors here. We get console wise. Home icon. Navigation.\u003C/p>\u003Cp>Yeah. This is the only bad part. I'm gonna zoom out just a little bit here so I can find\u003C/p>\u003Cp>Speaker 0: Just to actually navigate this, healthscape of tabs. Yeah.\u003C/p>\u003Cp>Speaker 1: Just delete a bunch of these icons. Gosh. They put so many icons in this.\u003C/p>\u003Cp>Speaker 0: Everyone that direct us has a personal choice between Mac and Windows, by the way. So, you know, some folks use use either. I would say with the folks I work with directly, I think we mostly use Max, but that's definitely not the case across the board. Tim is my resident, brain slug. Tim is is my resident Windows user.\u003C/p>\u003Cp>Oh, let's see. It's not about getting to use Linux. It's about wanting to use Linux. So folks wanna use it, whatever. Most of the stuff we use is in the cloud, so it doesn't really\u003C/p>\u003Cp>Speaker 1: matter. Yeah. Okay. Well, at least we got something now. Right?\u003C/p>\u003Cp>Alright. We're gonna ditch teams here at the bottom. Where are you? Your teams. Great.\u003C/p>\u003Cp>Just ditch that list of stuff.\u003C/p>\u003Cp>Speaker 0: Can you zoom back in? I'm like, I need my spectacles to see it over there.\u003C/p>\u003Cp>Speaker 1: Alright. Teams is gone. Right? So now we want to fetch the list of channels here and display those. So how do we do that, Kev?\u003C/p>\u003Cp>Speaker 2: Do you have the\u003C/p>\u003Cp>Speaker 0: Directus SDK in this app?\u003C/p>\u003Cp>Speaker 1: We do have the Directus SDK in this app.\u003C/p>\u003Cp>Speaker 0: Lovely stuff. Well, have you got it set up as, like, a as, like, a plug in, or do we need to do that now?\u003C/p>\u003Cp>Speaker 1: As a Nuxt plug in. So we're gonna Very good. We'll use dollar sign direct us, use Nuxt app. The only thing that I don't have is, like, the auto imports from the SDK. That was one of the nice things about the the other module I had.\u003C/p>\u003Cp>So we're gonna need to do, what, read items? Yeah. From atdirectus SDK.\u003C/p>\u003Cp>Speaker 0: I think that's it.\u003C/p>\u003Cp>Speaker 1: Speaking of this.\u003C/p>\u003Cp>Speaker 0: What's it complaining about? White space, I think. Yeah.\u003C/p>\u003Cp>Speaker 1: Yeah. These are, again, these are Rikes, ESLint, and\u003C/p>\u003Cp>Speaker 0: Which are very legit for building actually serious things unlike this.\u003C/p>\u003Cp>Speaker 1: They're very, very legit. Right? Alright. So let's do what do we want? We want channels.\u003C/p>\u003Cp>There's a ref for channels, and then we're going to well, actually, we should just be able to use Nuxt async data. Right? Nuxt data error. We get an error. We're gonna go, wait, use async data.\u003C/p>\u003Cp>We'll give this a key. Let's just call it channels. And then we just return something from Directus, right? So Oh,\u003C/p>\u003Cp>Speaker 0: look at that interesting code pilot code using old SDK methods. Fascinating.\u003C/p>\u003Cp>Speaker 1: Yeah. Not right. Right? That's a oh, what are we doing there? Auto completion or something.\u003C/p>\u003Cp>So we'll do read items. We've got channels. Rates. Oh, man. GitHub Copilot is getting on the way this time.\u003C/p>\u003Cp>Speaker 0: You can turn it off if you want.\u003C/p>\u003Cp>Speaker 1: We do only have an hour. We'll see. Alright. So, basically, we're gonna read all the items from channels. We wanna get the fields.\u003C/p>\u003Cp>We'll just use the wild card. It's basically just name and ID. Does that do it? It should do it. Let's just destructure this and oh.\u003C/p>\u003Cp>Speaker 0: Yeah.\u003C/p>\u003Cp>Speaker 1: Are you are you\u003C/p>\u003Cp>Speaker 0: As as channels.\u003C/p>\u003Cp>Speaker 1: Needs to be channels, and we'll just call this channels. And hopefully\u003C/p>\u003Cp>Speaker 0: And now now usually use channels.\u003C/p>\u003Cp>Speaker 1: Yep. We just go down here. Item in channels.\u003C/p>\u003Cp>Speaker 0: Very nice. And I think we could it is name. The key is name. Yeah.\u003C/p>\u003Cp>Speaker 1: Yeah. Item dot name. We're just gonna swap this for, like, Nuxt link. One of the interesting things, like, Nuxt link will take a h ref or, like, a 2 property. So that's one of the nice things that I like about it just because it's you know, you're used to using h ref on the regular side when you're creating a regular link.\u003C/p>\u003Cp>Right. Do you wanna do\u003C/p>\u003Cp>Speaker 0: you wanna know what's happened? We've, we've hit the 20 minute mark, and, thank gosh. I don't have to be here like, I can sweat from the audience. At this point, I'm gonna I'm gonna hand off to the amazing Alex who I'm gonna bring in. Hang on.\u003C/p>\u003Cp>Let let me add him in. Alex, would you, would you like to say hello?\u003C/p>\u003Cp>Speaker 2: Good evening, everyone. Can you hear me alright?\u003C/p>\u003Cp>Speaker 0: I can hear you grand. Lovely. Your your lovely tones. Excellent. I've\u003C/p>\u003Cp>Speaker 2: I've, enjoyed watching you sweat, Jacob. It's been great.\u003C/p>\u003Cp>Speaker 0: Oh, I'm sorry. It's coming it's coming for you too, mate.\u003C/p>\u003Cp>Speaker 2: Yeah. Yeah. Yeah. I'm I'm learning a lot of you right now as well. So\u003C/p>\u003Cp>Speaker 1: Hey, Viv. Hey. We failed to resolve next link. What is that all about?\u003C/p>\u003Cp>Speaker 0: I'm gonna peace out everyone. I'm gonna put you in Alex's very capable hands for 20 minutes, and then, he will be handing off to Matt. Bye for now, folks. Enjoy the rest. Yeah.\u003C/p>\u003Cp>Speaker 1: I thought we were doing, like, Royal Rumble rules. Everybody just hangs on. Be fun. Oh.\u003C/p>\u003Cp>Speaker 2: You just call me now, Brian.\u003C/p>\u003Cp>Speaker 1: I that's alright, my friend. How are you, dude? It's been a while since we've, like like paired up for something.\u003C/p>\u003Cp>Speaker 2: Yeah. Yeah. Yeah. This is this is cool, man. I, always enjoy, seeing all this view code, which I wish I understood better.\u003C/p>\u003Cp>Speaker 1: Yeah. Most of it is is tailwind at this point. But, yeah. So we having an error right now. Right?\u003C/p>\u003Cp>We're not having permission to access the channels. So we just need to log in. Right? So I'm gonna go to my login form components. Let's redirect to slash app.\u003C/p>\u003Cp>And we're gonna try to log in to this thing as well. I guess I need to add a user for you. Yep. I have one for Alex. Alright.\u003C/p>\u003Cp>Oh, don't forget the role. Alright. So I'm just gonna try to log in as Alex now. So we're gonna log in. Oops.\u003C/p>\u003Cp>Nope. That's not right. It's auth/login. I already used the link on the home page. That would have worked.\u003C/p>\u003Cp>So we got Alex at example, and the password is super secure. Okay. Alright. So now we can see at least we have our channels up here. Right?\u003C/p>\u003Cp>That looks good.\u003C/p>\u003Cp>Speaker 2: Yep. Yeah. Nice.\u003C/p>\u003Cp>Speaker 1: Alright. How are you feeling, Alex? Are you nervous as as Kevin? Like, feeling good? I\u003C/p>\u003Cp>Speaker 2: I've got a feeling you got the power of Bry Ross, under the hood there. So Ross. Big. I've got confidence, man. I got confidence.\u003C/p>\u003Cp>Speaker 1: Okay. Right. Alright. So now we need to fix, like, the h refs here. Like, hey.\u003C/p>\u003Cp>We wanna navigate to one of these channels. Right? Let's just test if this is gonna work. I'm a script at the top guy. Not sure if we've got any other view fans or not.\u003C/p>\u003Cp>I like seeing what's going on with the JavaScript first, and we're going to use the routes. So we'll do routes. Let's use routes. And I'm just gonna do this. I'm just gonna log this out.\u003C/p>\u003Cp>Right? I wanna look at routes a params.channel. K. Good. The other thing that we're gonna need to do somewhere in here where's the main dev?\u003C/p>\u003Cp>Main content. We're gonna plug in this Nux page here. So this should render the child page, for all my nested routes. Yeah. Alex is a boss.\u003C/p>\u003Cp>So, I'm not sure if that's John Daniels or not, but, I will say, like, if you've used the template CLI tool at all, Alex is is that guy. He's the guy that put that together originally.\u003C/p>\u003Cp>Speaker 2: Yeah. Thanks thanks to directors having good APIs. It was, yeah, difficult, but we we got there, and it's pretty cool right now.\u003C/p>\u003Cp>Speaker 1: Yeah. Alright. So we got the next page. I save. 2 2 what's going on?\u003C/p>\u003Cp>Oh, yeah. I forgot to fix the login again. Yeah. Apparently.\u003C/p>\u003Cp>Speaker 2: Yeah.\u003C/p>\u003Cp>Speaker 1: You need to persist that. Alright. So what else do we need to do? We need to fix those links. Channels.\u003C/p>\u003Cp>Where are you? Alright. So this will be channel or item dot name.\u003C/p>\u003Cp>Speaker 2: Item dot name. Yeah.\u003C/p>\u003Cp>Speaker 1: And then we'll do /channels /item.name. Should get it. We'll do a Nuxt link. Oh, duh. It's app dot channels.\u003C/p>\u003Cp>Speaker 2: Nice.\u003C/p>\u003Cp>Speaker 1: So now we have our channels working. Right? What are we gonna do inside the channels? Right? Great call.\u003C/p>\u003Cp>What do we do inside the channels? We are going to here is this is where we're actually gonna fetch, like, real time data. Right?\u003C/p>\u003Cp>Speaker 2: Yeah. You think? Yeah. Yeah. I'm looking forward to this part, the the, real time.\u003C/p>\u003Cp>Speaker 1: Okay. Alright. So I'm gonna cheat a little bit. We're just gonna go into our documentation. We do have this nice guide, if you haven't checked it out, on real time multi user chat.\u003C/p>\u003Cp>There's a vanilla JavaScript version. There's React. There's Vue. Let's just take a look at this. Right?\u003C/p>\u003Cp>Alright. How are we going to authenticate with real time? So we've got, the authentication composable. We've added the real time composable. All of those are in my plug in here.\u003C/p>\u003Cp>So it should be as simple as, by calling connect. That'd be right. Let's see. Kevin's got a I think it was Kevin that he's got. We've got a subscribe function here.\u003C/p>\u003Cp>So we're gonna subscribe to the messages, client dot connect. Yep. Let's see here.\u003C/p>\u003Cp>Speaker 2: This is usually where I run into some SSR issues, but I'll we'll see how you manage this one, Brian.\u003C/p>\u003Cp>Speaker 1: Let me show you. SSR equals false.\u003C/p>\u003Cp>Speaker 2: That's That's the solution there?\u003C/p>\u003Cp>Speaker 1: That's the easiest way. Yeah. Yeah. Absolutely. Alright.\u003C/p>\u003Cp>Alright. So let's just, is this cheating? Yeah. This probably is. Use whatever you have at your disposal.\u003C/p>\u003Cp>Right? So the subscription client dot subscribe messages, that's correct. Our fields here that we want, we're grabbing the all the fields on the root level. I guess I could zoom in a little bit and make that easier for everybody. Then we have user.\u003C/p>\u003Cp>So let's just grab all the fields from the user. We've got message of subscription, and then it looks like there's a receive message function that goes along with this.\u003C/p>\u003Cp>Speaker 2: Yeah. You yeah, you might need to filter on the channel as well, but maybe we can do that in the next step.\u003C/p>\u003Cp>Speaker 1: Yeah. So we've got data type equals subscription. This is just like the init. Right? Let's see if we are getting any.\u003C/p>\u003Cp>No. Yeah. This is gonna be interesting. I should have set up better authentication. So persisted on this.\u003C/p>\u003Cp>Alright. Do we see anything from, see some more issues from Tailwind? X mark icon. Where are you? Note to self, on the next one, do not import any of the icons from.\u003C/p>\u003Cp>So this should be, like, we definitely wanna redirect on this. Right? Let's add app. Let's add some page meta. Define oh, actually, it's not a const.\u003C/p>\u003Cp>We'll just do define page meta. And if we give this a middleware of auth, it should automatically redirect. And then I'm gonna go into where's my login form? I'm gonna make this really easy for myself, Alex.\u003C/p>\u003Cp>Speaker 2: Oh, good idea.\u003C/p>\u003Cp>Speaker 1: And password, super secure. We'll learn a little bit about everything here. Okay. So it's redirected. Sign in.\u003C/p>\u003Cp>This should take us back to the app, where we could get all the channels. What am I not seeing here? Right. Do we have to do we need to connect to the client, I guess? Let's plug in why we're not receiving\u003C/p>\u003Cp>Speaker 2: Yeah. Client dot connect. Right? Isn't that it?\u003C/p>\u003Cp>Speaker 1: Okay.\u003C/p>\u003Cp>Speaker 2: Yeah.\u003C/p>\u003Cp>Speaker 1: Yeah. So this is on mounted, but we're not using SSR. This should just work. Right? Where are we gonna stick that?\u003C/p>\u003Cp>On? Oh, what if we just call subscribe? Set up all these functions, forgot to call them. Alright. Client is not defined.\u003C/p>\u003Cp>Duh. Because it's not the client, it should be directus dot subscribe. So, again, we're gonna grab that directus plugin that we set up from use Nuxt app. It's error data is not defined. Oh, we gotta pass the data to that.\u003C/p>\u003Cp>Depcription started. Okay. Alright. So what do we wanna do now? Within the channel, we wanna have all of our messages.\u003C/p>\u003Cp>Right? I'm assuming that will be an array.\u003C/p>\u003Cp>Speaker 2: Sounds good to me.\u003C/p>\u003Cp>Speaker 1: In our received messages Actually, let's just do this to start with, see what type of data we're getting back. Received message. Alright. So now if we go in, I got, like, 35 tabs here, don't I?\u003C/p>\u003Cp>Speaker 2: Don't we all?\u003C/p>\u003Cp>Speaker 1: For apps. For hours. Live.\u003C/p>\u003Cp>Speaker 0: Or I\u003C/p>\u003Cp>Speaker 1: forgot the URL. It's just 100 apps dotdirectus.app. And I've logged myself out again. Let's keep that.\u003C/p>\u003Cp>Speaker 2: I can't help you here, Brian. Okay.\u003C/p>\u003Cp>Speaker 1: Alright. So now how do I split these out? That's the only thing I get hung up on on our half the time. It's like, hey. How do we make this, go split screen?\u003C/p>\u003Cp>Alright. So couple things should be happening here. Right? We've got our subscription. Let's make this larger.\u003C/p>\u003Cp>Right. So the subscription has started. We're in the Kevin is crazy crazy nervous. We'll probably have to filter that on our front end anyway, but, testing 1, 2, 3. We'll just add the channel to begin with.\u003C/p>\u003Cp>And Oh. Cool. So we we see the real time is connecting. We're getting our data. There's the actual messages.\u003C/p>\u003Cp>Great. Right. We've got the test. We've got the user is null.\u003C/p>\u003Cp>Speaker 2: Probably permissions.\u003C/p>\u003Cp>Speaker 1: Correct. That is correct. Alright. We need to make sure that the user role can access all the users. So right now, it's just by default, like, just a one user.\u003C/p>\u003Cp>We'll just make it so we can read all the users, and let's send a new one. Test 567. Save. Get our message. Okay.\u003C/p>\u003Cp>Now do we get the user data? Okay. Cool. That's a lot more user data than we actually wanted. So let's go in and trim that down.\u003C/p>\u003Cp>We'll just do first name, last name, avatar. We probably want the ID too.\u003C/p>\u003Cp>Speaker 2: And the\u003C/p>\u003Cp>Speaker 1: dot ID.\u003C/p>\u003Cp>Speaker 2: And the first name, not the first name.\u003C/p>\u003Cp>Speaker 1: First name.\u003C/p>\u003Cp>Speaker 2: Not the\u003C/p>\u003Cp>Speaker 1: catch. Good catch. Alright. Cool. And then here, we need to what?\u003C/p>\u003Cp>We want to see the message. Where's that message at? Alright. Subscription, create data. We want to filter that.\u003C/p>\u003Cp>Right? What are we gonna call this? For each message of subscription, We need to populate that data. Right?\u003C/p>\u003Cp>Speaker 2: Do do you wanna filter it at the at the WebSocket level or just at the whenever a new message is received? You could do both, I guess. But\u003C/p>\u003Cp>Speaker 1: Yeah. I I'm trying to think of, like, in Slack, you'd probably have, like, some kinda indicator up here that's, like, hey. Yeah. Yeah. Even if a message in a different channel popped up, you would probably filter it out on the the front end.\u003C/p>\u003Cp>Right? Yeah. On on the specific channel Because you probably have, like, some type of yeah. Alright. So receive message here.\u003C/p>\u003Cp>What do we does it show the oh, let's get the channel name as well. Query Oh, yeah. That's that'd be a useful one. Dotidchannel.name. So here, if the data type equals subscription.\u003C/p>\u003Cp>Create. And data event equals create. We will push those messages into our array of messages. And then instead of this, we should be showing a some messages. Alright.\u003C/p>\u003Cp>What we doing on time? I've totally lost the window here.\u003C/p>\u003Cp>Speaker 2: Yeah. Me too. Me too. It's about quarter to the hour on my clock. Let's\u003C/p>\u003Cp>Speaker 1: just take a look. 22 minutes left. Okay. Alright. Cool.\u003C/p>\u003Cp>Fun. Fun. Fun. Alright. GitHub Copilot for the win.\u003C/p>\u003Cp>Do we how confident are we feeling? I can't even see the chat at this point.\u003C/p>\u003Cp>Speaker 2: But Not confident in that one.\u003C/p>\u003Cp>Speaker 1: So we'll do, like, a list for all the message. Yeah. Again, hey. Like, you gotta take it with a grain of salt. Message dot content.\u003C/p>\u003Cp>You know, I I think this would get better if we had typed this out, but Yeah. Key message dot ID, we should have that for sure. We'll just close that list, see what we get. Alright. Do we have any messages?\u003C/p>\u003Cp>The other thing is, like, do we fetch the messages for this channel on\u003C/p>\u003Cp>Speaker 2: Initially. Right?\u003C/p>\u003Cp>Speaker 1: On yeah. And, like, do you have you messed with the the real time and, like, the chat stuff at all, Alex, or no?\u003C/p>\u003Cp>Speaker 2: Not not a not as much as I should have, but, this is an interesting one because you might not even need to use the real time in this in this example.\u003C/p>\u003Cp>Speaker 0: Just Hello, everyone. It's the disembodied voice of Kevin here. Hello. I'm watching, I've been screaming into the void in chat. I know this isn't I know this is an illegal move to throw you a bone here, but the init payload comes with the initial items.\u003C/p>\u003Cp>Up to a 100, and you can set limit minus 1 to get more than a 100. Okay. Peace out. Bye. What was that, Alex?\u003C/p>\u003Cp>You wanna miss off the clock, mate.\u003C/p>\u003Cp>Speaker 2: Okay. Thanks, Gabe. I think that's probably just, just we you can start logging out the the all all the messages that come through from the website. Right? So it's probably a yeah.\u003C/p>\u003Cp>So it would be one of the messages received maybe, with the initial with a different type. I don't know if I've seen that in your logs to be fair. But\u003C/p>\u003Cp>Speaker 1: Yeah. Let's back up. Subscription started. Received message. No.\u003C/p>\u003Cp>Matt's gonna save the day on this.\u003C/p>\u003Cp>Speaker 2: We should know this. I should know this, Bryant.\u003C/p>\u003Cp>Speaker 1: That's alright. So, like, we we'd also do it this way as well. Right? Where we go in and read items. Oh, nope.\u003C/p>\u003Cp>That's gonna be coming from the SDK. Read items from directus s e k. Let's just call this\u003C/p>\u003Cp>Speaker 2: Oh, I think no. I'm starting to sweat. Thanks, Kevin.\u003C/p>\u003Cp>Speaker 1: Yeah. No worries. God. What am I doing? Constant populate messages equals async.\u003C/p>\u003Cp>Yeah. There we go. This is wrong. Totally wrong. Oh, no.\u003C/p>\u003Cp>That's not that bad, actually. Filter channel equals route dot params.channel. Equal route dot value. No. Should just be route dot is it route dot value?\u003C/p>\u003Cp>Is route\u003C/p>\u003Cp>Speaker 0: reactive? Hello. Hello, everyone. It's me again. It's Kevin again.\u003C/p>\u003Cp>I was I was having the chat with Matt, and in the nature of chaos, we've decided to Royal Rumble this. So it is my pleasure to introduce mister Matt Minor. Hello, Matt.\u003C/p>\u003Cp>Speaker 3: Hey, everyone. I, will be the most useless person on this call because I'm not\u003C/p>\u003Cp>Speaker 0: Oh, hardly.\u003C/p>\u003Cp>Speaker 3: I'll help you pick the colors. And I was actually thinking, maybe I could just sabotage Bryant here at the end.\u003C/p>\u003Cp>Speaker 1: Just Sabotage.\u003C/p>\u003Cp>Speaker 3: The headlines of the day or something. Do it,\u003C/p>\u003Cp>Speaker 0: man. Oh, no. Don't don't do that. Can you imagine?\u003C/p>\u003Cp>Speaker 3: What do we have? We have we have 15 minutes.\u003C/p>\u003Cp>Speaker 0: No. There's there's 20 left. We got just just 20 minutes left. We're 2 thirds of the way in. No pressure.\u003C/p>\u003Cp>But it it doesn't look like Slack yet, does it? Where are the messages? Yeah. Alex, is it is this what you did with your 20 minutes, mate?\u003C/p>\u003Cp>Speaker 2: Mate, we were we were work we were working hard. Yeah. We were working hard. All hardly working.\u003C/p>\u003Cp>Speaker 1: Yeah. Yeah. This was\u003C/p>\u003Cp>Speaker 3: where are we stuck?\u003C/p>\u003Cp>Speaker 1: What are\u003C/p>\u003Cp>Speaker 3: we, what are we doing?\u003C/p>\u003Cp>Speaker 1: Just gonna load the initial messages.\u003C/p>\u003Cp>Speaker 0: So so I know from experience that when you I don't know what's up with maybe how you're including real time here, but that init payload that init message does come. It definitely comes with all of the mess with all of the items in the collection, but I have also observed it's not happening here. So I think that would be the easiest fix is, like, why isn't that working? Because then you can just use the real time interface.\u003C/p>\u003Cp>Speaker 1: Right. Why isn't that working\u003C/p>\u003Cp>Speaker 2: Yeah. Kevin?\u003C/p>\u003Cp>Speaker 0: Yeah. Bloody great question, mate.\u003C/p>\u003Cp>Speaker 1: So I log in. We go to Kevin is crazy nervous. We start this subscription.\u003C/p>\u003Cp>Speaker 0: Yeah. But where is the where's the payload? Where's that init payload? So in oh, oh, I think maybe it Oh,\u003C/p>\u003Cp>Speaker 1: is it is it here?\u003C/p>\u003Cp>Speaker 0: Is it is it because you're just subscribing to the single event? If you remove the event from, like, inside of the, if you get rid of that and get every everything over that subscription, does that help? There it is.\u003C/p>\u003Cp>Speaker 1: Data. Oh, okay. Yep. Okay. So just use that.\u003C/p>\u003Cp>Nice. Okay. There we go. My man.\u003C/p>\u003Cp>Speaker 3: That's what I was gonna say too. But, Kevin\u003C/p>\u003Cp>Speaker 0: I know. Sorry. I felt I felt emanating from your your corner of the ring. How are you doing today, Matt?\u003C/p>\u003Cp>Speaker 1: Is I'm good. I'm doing good.\u003C/p>\u003Cp>Speaker 3: I'm excited to be\u003C/p>\u003Cp>Speaker 0: here. Good. News headlines for today?\u003C/p>\u003Cp>Speaker 3: Facebook and Instagram. No. Struggling.\u003C/p>\u003Cp>Speaker 0: I I sorry. I was too busy working, so I didn't know that.\u003C/p>\u003Cp>Speaker 1: Oh, yeah.\u003C/p>\u003Cp>Speaker 3: Sorry. I was too I, it just came up\u003C/p>\u003Cp>Speaker 2: when\u003C/p>\u003Cp>Speaker 3: I was logging in to work today. So, how's the chat doing? Is anybody Let's see.\u003C/p>\u003Cp>Speaker 2: You you might have to spread that one, Bryant. I think.\u003C/p>\u003Cp>Speaker 0: Yeah. Yeah. You will. You will.\u003C/p>\u003Cp>Speaker 3: Just enjoying the chaos. Yeah. But for a net\u003C/p>\u003Cp>Speaker 0: but for a net, dude, for a net, don't push it into the array. Just replace the array because\u003C/p>\u003Cp>Speaker 1: Oh, okay.\u003C/p>\u003Cp>Speaker 0: There's gonna be nothing in it at the start.\u003C/p>\u003Cp>Speaker 1: Data dot data.\u003C/p>\u003Cp>Speaker 0: Joshua, just enjoying the cast. Honestly, so are we. As I said, this is our first kind of live event of this kind, and I don't know. I'm having a great time. It is chaotic, and we're just making it up.\u003C/p>\u003Cp>We changed the format. Who bloody cares? Love it.\u003C/p>\u003Cp>Speaker 1: Yeah. Nope. Alright. So now we got some messages. Right?\u003C/p>\u003Cp>Let's, let's mess with this a bit. Alright. So we're gonna have a div. We're gonna show the avatar, image source,\u003C/p>\u003Cp>Speaker 0: message. Joshua, it's so funny. You're like, yeah. You know, I'm struggling, like, getting this, you know, flow automation thing to work while listening to you in the background. And the funny bit is some of the people who would be there to help you are currently here.\u003C/p>\u003Cp>So it's like, have fun struggling, but you're struggling on your own for the next, 15. Nah. Jonathan's there. Funny. And\u003C/p>\u003Cp>Speaker 3: you think you're struggling\u003C/p>\u003Cp>Speaker 1: to get, like,\u003C/p>\u003Cp>Speaker 3: 4 people watching you work at the they were all just staring over Brian Schuler right now.\u003C/p>\u003Cp>Speaker 1: I love it, man. Thank you. This is this is this is more fun for me. I think I feel\u003C/p>\u003Cp>Speaker 3: like I go, when I go shopping with my wife and, like, just follow her around the store. That's what I feel like right now. I know value just in the way.\u003C/p>\u003Cp>Speaker 0: Alright. So far.\u003C/p>\u003Cp>Speaker 1: Let's add some padding for these. What else?\u003C/p>\u003Cp>Speaker 0: We have time for the padding. We have time for the padding. Dude. I'm good. God.\u003C/p>\u003Cp>I'm sweating again.\u003C/p>\u003Cp>Speaker 1: All things, man. All things. You you gotta have time for it. Alright. So we got some messages.\u003C/p>\u003Cp>Great. Now we need what? We need a form at the bottom of this one. Right? So we got VText.\u003C/p>\u003Cp>What does this thing do? This is, just text. Okay. Great. We don't need that.\u003C/p>\u003Cp>So we just got an input. Text input, b model, new message. Right? So we're gonna populate. We can just scrap all this shit.\u003C/p>\u003Cp>Oh, were we allowed to curse on this one or no?\u003C/p>\u003Cp>Speaker 0: Well, you've done it now. So yeah. Sure. Anything anything goes.\u003C/p>\u003Cp>Speaker 1: Anything goes. Alright. So we got a new message. We're just gonna add a ref for that. We will remodel new message at key up, enter, send message.\u003C/p>\u003Cp>Yeah. I don't like that, but, let's just wrap it. We'll give it a button. Oh, actually, I forgot I got the Nux UI library included in this. So we should be able to get something nice just by doing new input.\u003C/p>\u003Cp>Did that work? No. Nope. Did not work.\u003C/p>\u003Cp>Speaker 2: No pressure, Brian. That's\u003C/p>\u003Cp>Speaker 1: the beauty of this. No pressure.\u003C/p>\u003Cp>Speaker 0: Yeah. We've all gone quiet. Just letting you work\u003C/p>\u003Cp>Speaker 1: for a moment. New button. Just add send message. What are we doing now? Nope.\u003C/p>\u003Cp>Gotta close that guy. Alright. So we got this. Right? We need to add a handler for this.\u003C/p>\u003Cp>Just click send oh, let me just remove that. Send message.\u003C/p>\u003Cp>Speaker 0: Nice.\u003C/p>\u003Cp>Speaker 1: And then we've got should have an async function for send message. And this is not right at all. Right? So I think\u003C/p>\u003Cp>Speaker 0: I don't know. How are you gonna tap into the because you you've gotta you've got to send it over the subscription, don't you?\u003C/p>\u003Cp>Speaker 1: Yeah. That's that's where I was going back to your wonderful guy.\u003C/p>\u003Cp>Speaker 0: Oh, no. No. No. I think it's fine. No.\u003C/p>\u003Cp>No. No. Ignore me. It's totally fine. You can do it.\u003C/p>\u003Cp>You can do it at that level where you've written send message because received message is at that level too. Right? So Actually Actually\u003C/p>\u003Cp>Speaker 1: client.send message. Right? Should be possible or no?\u003C/p>\u003Cp>Speaker 0: We'll find out in a minute. I think there's gonna be a weird scoping thing here. Let's find out.\u003C/p>\u003Cp>Speaker 2: Well, I think if you just use the create item function with the new SDK,\u003C/p>\u003Cp>Speaker 0: that should work. Do that,\u003C/p>\u003Cp>Speaker 1: but you could also\u003C/p>\u003Cp>Speaker 0: send it over you could send it over the WebSocket connection, and then you're just using that one connection use make a make a HTTP request, honestly.\u003C/p>\u003Cp>Speaker 1: Test. Oh, yeah. That's why you don't trust this. Right?\u003C/p>\u003Cp>Speaker 0: Yes.\u003C/p>\u003Cp>Speaker 1: Request. Yeah. Create item.\u003C/p>\u003Cp>Speaker 2: Yeah.\u003C/p>\u003Cp>Speaker 1: Did you get it? Test. Send message. Alright. What do we get?\u003C/p>\u003Cp>Unexpected error occurred.\u003C/p>\u003Cp>Speaker 0: Good. Good. Good. Helpful. Helpful.\u003C/p>\u003Cp>Helpful.\u003C/p>\u003Cp>Speaker 2: That's the channel that's the channel name. That should be the ID. Right?\u003C/p>\u003Cp>Speaker 1: Yes. Correct.\u003C/p>\u003Cp>Speaker 2: Oh, did we get the channel ID?\u003C/p>\u003Cp>Speaker 0: No. That's right. Because, it's in the it's coming from the URL.\u003C/p>\u003Cp>Speaker 2: Yeah. But it's\u003C/p>\u003Cp>Speaker 0: That value there is coming from the URL. Yeah. Yeah. The dynamic part.\u003C/p>\u003Cp>Speaker 1: But the channel itself\u003C/p>\u003Cp>Speaker 0: Oh, wait. Right. Right. It's saying here, ding, ding, Alex. Got it.\u003C/p>\u003Cp>So hang on. I lower my confidence.\u003C/p>\u003Cp>Speaker 1: There you go. Yeah. Yeah. So we gotta get the channel ID. Where do we have the data?\u003C/p>\u003Cp>Right?\u003C/p>\u003Cp>Speaker 0: Oh, I understand now. I understand what you mean the ID. Got it. Got it. Got it.\u003C/p>\u003Cp>So I I think you'll need to do that. That initial load. Yeah. I think I think you do. Yeah.\u003C/p>\u003Cp>Just to fetch that ID.\u003C/p>\u003Cp>Speaker 1: I really don't wanna do that. But\u003C/p>\u003Cp>Speaker 0: Mate, you've got you've got 10 9 minutes. Oh, we got left? You're doing that.\u003C/p>\u003Cp>Speaker 1: We're doing it. Yeah. Alright. So read items, channels, filter ID equals no. That's our ID.\u003C/p>\u003Cp>Speaker 2: What's the channel name? Right? Or\u003C/p>\u003Cp>Speaker 1: Channel name. This will be wrapped. We'll have what equals. Alright. So you can have the channel.\u003C/p>\u003Cp>There's the ID. Good.\u003C/p>\u003Cp>Speaker 2: And it's an array out, which is a rookie error that I often make.\u003C/p>\u003Cp>Speaker 1: Woah. Yep. That is. Yeah. So this will be\u003C/p>\u003Cp>Speaker 0: How are you doing over there, Matt? Having a good time?\u003C/p>\u003Cp>Speaker 3: Watching all along. Yeah. I'm waiting for us to get to the colors. I think that's where I'll have my most, impact.\u003C/p>\u003Cp>Speaker 0: Well, we've got we've got a whole 8 minutes, and I think we're about to be finished, I guess. So, you you get a pick of the next function now or 2.\u003C/p>\u003Cp>Speaker 1: If I use the, what, If we use async data, and you can do, like, some transforms on this as well. Using data. Channel. Turn up paste. Is this guy out?\u003C/p>\u003Cp>Speaker 2: I think I think Greg's suggestion was the best to just use the channel ID in the in the URL.\u003C/p>\u003Cp>Speaker 1: Probably. 100%. Requests from that. This needs to be here. I like the pretty URLs, though.\u003C/p>\u003Cp>Alright. I will just refresh. Go to crazy nervous. Okay. So now we've got the channel.\u003C/p>\u003Cp>Alright. So we got channel. This should just be the channel dot ID. Best. Okay.\u003C/p>\u003Cp>So we're okay. It seems that, like, the message was sent, unless I'm wrong. Test. Okay. So it did populate the message.\u003C/p>\u003Cp>We just don't see it show up here. Alright. So what are we doing wrong there?\u003C/p>\u003Cp>Speaker 2: In that payload, shouldn't it also have the the channel ID?\u003C/p>\u003Cp>Speaker 1: Yep. It does.\u003C/p>\u003Cp>Speaker 2: But it does, but it's not showing in the console there.\u003C/p>\u003Cp>Speaker 1: Test. Okay. There's the test. Send messages. Yeah.\u003C/p>\u003Cp>That's kinda odd. Why is it not showing that in the payload? Alright. Regardless, we should be getting a message back. Right?\u003C/p>\u003Cp>Reading undefined dot avatar. This is a simple v f. Right? Yes. Message.\u003C/p>\u003Cp>Speaker 0: Just get rid of it. No one needs an image. No one needs an image. We're using Slack compact mode. Ditch it off.\u003C/p>\u003Cp>Get rid of it. Just saying that's 5 that's 5 and a half minutes.\u003C/p>\u003Cp>Speaker 1: Yeah. No worries, man. Alright. Test. Alright.\u003C/p>\u003Cp>So first name. Why is this not populating? User but we gotta have the username. Right? It cannot read first name.\u003C/p>\u003Cp>Speaker 0: It's not user. It's user created or whatever you called it. Right? Or did you rename it to user?\u003C/p>\u003Cp>Speaker 1: No. It should be user created. Yeah. Channel messages dotuser.firstname. This is used, like, a v f message.\u003C/p>\u003Cp>A user? You're trying to get something out of this. Oh, shoot. Blah blah blah. Kevin is crazy nervous.\u003C/p>\u003Cp>Still crazy nervous.\u003C/p>\u003Cp>Speaker 0: I'm literally rocking back and forward in my chair here by feeling\u003C/p>\u003Cp>Speaker 1: the heat. So, like, the the messages are coming in, but we're just not populating. We missed something. Messages dot value dot push data dot data. Alright.\u003C/p>\u003Cp>So if we're receiving data, we're getting data. It's it's\u003C/p>\u003Cp>Speaker 2: a it's another array in there.\u003C/p>\u003Cp>Speaker 1: Data is an array. Yeah. Okay. So, we'll just do this.\u003C/p>\u003Cp>Speaker 0: Someone else who has the cute baby in the background, and I'm using a hardware mute, but, I think that's that's me. And they're they're they're hungry. They're not.\u003C/p>\u003Cp>Speaker 2: Sorry. That might be my fault.\u003C/p>\u003Cp>Speaker 0: Oh, do you I haven't heard I haven't heard yours in the background.\u003C/p>\u003Cp>Speaker 2: Okay. Was it yours? Alright.\u003C/p>\u003Cp>Speaker 0: Could be.\u003C/p>\u003Cp>Speaker 2: They came\u003C/p>\u003Cp>Speaker 0: home, but they're not sounding cute. They're not sounding cute. They're sounding angry.\u003C/p>\u003Cp>Speaker 1: Alright. So now what is the Nuxt command? Right? I think there's one where I can let you guys tunnel into this. Right?\u003C/p>\u003Cp>Nuxt dev tunnel? Where is this guy?\u003C/p>\u003Cp>Speaker 0: That is brand that must be brand new because I didn't know that was there. But it's built into Visual Studio Code. If you bring up your status bar at the bottom, I thought it was baked in now. No way. That is sick.\u003C/p>\u003Cp>Alright. So\u003C/p>\u003Cp>Speaker 1: does this actually work?\u003C/p>\u003Cp>Speaker 0: There there's the tunnel right there, the Cloudflare URL. Womp womp.\u003C/p>\u003Cp>Speaker 1: Why Why doesn't it work?\u003C/p>\u003Cp>Speaker 0: You better work it out because you got 2 minutes.\u003C/p>\u003Cp>Speaker 1: At this point, I don't know. Do you guys just, do me a favor and then log in to the direct us instance in this firehouse message?\u003C/p>\u003Cp>Speaker 0: I got you.\u003C/p>\u003Cp>Speaker 1: P p m. Yeah. Nux. Dev. Oh, p m p m.\u003C/p>\u003Cp>It would be, like, running the let's see. I don't know if that's it or not. I'm stressed. Yeah. So there we go.\u003C/p>\u003Cp>Yeah.\u003C/p>\u003Cp>Speaker 0: That was me.\u003C/p>\u003Cp>Speaker 1: It's Does it work\u003C/p>\u003Cp>Speaker 0: does it work if you type it in here as well? Yes.\u003C/p>\u003Cp>Speaker 1: I I'm logged in as Alex. I'm logged in as Alex. Alright. Send send one more just so we can say something.\u003C/p>\u003Cp>Speaker 0: Alright.\u003C/p>\u003Cp>Speaker 1: Here? Less stressed. Yay.\u003C/p>\u003Cp>Speaker 0: Yeah. Rock\u003C/p>\u003Cp>Speaker 1: on. Some concept.\u003C/p>\u003Cp>Speaker 0: That's basically feature complete.\u003C/p>\u003Cp>Speaker 3: I was shitting. Hit right at the buzzer. Is Let's go.\u003C/p>\u003Cp>Speaker 1: Because this is a winner. Yeah?\u003C/p>\u003Cp>Speaker 0: Yay. Holy heck. My heart rate. I'm I'm gonna need to take a shower. I'm sweating.\u003C/p>\u003Cp>Oof. Look at that.\u003C/p>\u003Cp>Speaker 1: Yeah. Wow.\u003C/p>\u003Cp>Speaker 0: Stop the timer.\u003C/p>\u003Cp>Speaker 1: Yeah. Leave it for Stop. Yeah. Series a already. I've watched them, man.\u003C/p>\u003Cp>Speaker 0: And don't forget, there's more in your stressing and dressing in 100 hours. A whole season now available on Directus TV with more coming in April.\u003C/p>\u003Cp>Speaker 1: I I look forward to the other episodes where you guys are are it may be not on there. I I've enjoyed this a lot, though. This is fun. It definitely distracted a little bit.\u003C/p>\u003Cp>Speaker 0: Yeah. I did feel like, should this be, like, 1 app in a 100 hours, but with guests? Oh, wow.\u003C/p>\u003Cp>Speaker 1: Oh, so takeaways from this. Right? What did we what did we learn?\u003C/p>\u003Cp>Speaker 0: I'm not coming back to the next one.\u003C/p>\u003Cp>Speaker 3: Spend more time with Patty.\u003C/p>\u003Cp>Speaker 1: More time with that. Dude, I'm sorry we didn't get to the, like, colors. Like, do you wanna what what color are you feeling, Matt? It's the only reason I'm so sorry. Pick the color.\u003C/p>\u003Cp>I'm looking for the color.\u003C/p>\u003Cp>Speaker 3: I can't think of the hex code\u003C/p>\u003Cp>Speaker 1: for\u003C/p>\u003Cp>Speaker 3: I didn't 466, double f.\u003C/p>\u003Cp>Speaker 1: What do you want? 4466\u003C/p>\u003Cp>Speaker 3: f f.\u003C/p>\u003Cp>Speaker 0: No. It's 6644 f f.\u003C/p>\u003Cp>Speaker 3: Knew it. Hopefully, Ben's not watching.\u003C/p>\u003Cp>Speaker 0: What is it? Oh, he's watching. 6:6:8. He's watching.\u003C/p>\u003Cp>Speaker 1: 44 f f. Where are we? Is it not updating? Is that already the indigo oh, there it is. Okay.\u003C/p>\u003Cp>No?\u003C/p>\u003Cp>Speaker 0: Maybe it wasn't on it here. Is that the sidebar?\u003C/p>\u003Cp>Speaker 1: Yeah. Okay. Sidebar. Yeah. There you go.\u003C/p>\u003Cp>BG you're gonna find me one more time. 6644 f f? Yep.\u003C/p>\u003Cp>Speaker 0: That's the one.\u003C/p>\u003Cp>Speaker 1: That's it. There it is.\u003C/p>\u003Cp>Speaker 0: Subtleship. Boom. Feature comp New product. Ship it. Thanks everyone for joining us.\u003C/p>\u003Cp>Speaker 1: Yeah.\u003C/p>\u003Cp>Speaker 0: I'm not I'm not kidding. We probably won't do another one now. We probably will, but I won't be here. Gosh. Literally, 30 seconds to go.\u003C/p>\u003Cp>30 seconds to go. Congratulations, Brian.\u003C/p>\u003Cp>Speaker 1: Yeah. I feel like we started off on the wrong foot with the 5 minutes of technical difficulties. The for the nodes. Selecting 5 minutes of technical difficulties at the the start of this thing. That's what everybody did not see on the front\u003C/p>\u003Cp>Speaker 0: end. We got there. We got there.\u003C/p>\u003Cp>Speaker 1: Now we're now we're ready to code for, like, another 2 hours. And if you're\u003C/p>\u003Cp>Speaker 0: watching this live, this whole episode will be packaged up as a special at the end of season 1 of a 100 and a 100 hours on director's TV. We'll pop that out tomorrow. And then there are new episodes of a 100 apps and a 100 hours coming in April.\u003C/p>\u003Cp>Speaker 1: Amazing. Excellent. Yeah. Well, thanks for joining us, to the circus episode of 100 Hours 100 Hours. I'm your host, Brian Gillispie.\u003C/p>\u003Cp>Thanks to my special guests, mister Kevin Lewis, mister Avdv, and Awesome. With the crazy colors coming in.\u003C/p>\u003Cp>Speaker 0: Thank you, everyone. Bye for now.\u003C/p>\u003Cp>Speaker 2: Cheers. Bye. Cheers.\u003C/p>","Build in an extra 15 minutes for chaos. I'm gonna be here for the first 20. Then for the 1st presales engineers, Alex, will be here for 20, and then Matt will be here for 20. With that in mind, Bryant, I'm gonna start a timer. No stress, but stress. Take it away. Yeah. Let's start the timer. What are we building? You tell me. Yeah. So you you let me know ahead of time, maybe, like, just a hour ago that we were gonna be building. Any guesses in the chat. Right? This might be fun to do live. Right? What are the guesses? No guesses. Alright. I don't see him coming through. We are going to be building a Slack clone. Build direct to us. In an hour. No. In an hour. Alright. So, Slack. Right? Yeah. Everybody uses it. You either love it, hate it. Xbox Live. Yeah. We'll do Xbox Live next time. Alright. So let's sketch this thing out. Right? What do we need out of a Slack clone? What feels good as far as functionality for this? How are we gonna set it up in direct us? Facebook, they need some help with their APIs. Yep. Certainly. Yeah. Experienced that. We all saw that today. What is needed for Slack? I suppose the lightest version is gonna be you have channels, and inside of channels, you have users, and then you have channels. And then inside the channels, you have messages with, like, real time. And I think the that's the slimmest version. Right? Yeah. So let's put, like, channels and messages at the top. We would call, like, threads a stretch goal. Yeah. Thread threading feels like, probably more than an hour. Alright. So we got channels. We got messages. We're gonna get the user authentication from direct to its users. And as far as functionality, what do we wanna do? We wanna have a channel. We want to submit messages in the channel. We want that all to update in real time. Notifications? Do we want notifications? We'll I never wanna be notified when people send me messages ever. There is literally no world where I want that feature. So so how many Slack groups are you a part of where, like, you just got the whole thing muted, Kevin? That would be a good question. So let's take a look here. Let me let me open Slack for the first time in a in a hot minute. Okay. I'm in 123456789101112 Slacks. 12 Slacks. And I look at 2 of them regularly. 3 of them regularly. Most of the others are muted. Alright. Yep. That's how it goes. Same thing for me. Alright. So I've got a brand new instance. Please don't use this password. Oh my lord. I okay. That's fine. Everything's fine. Probably need to change that before we get some surprises. Right? Change that real quick. Okay. Alright. So we're inside Directus. How are we gonna map this out? Right? We get the Directus users already out of the box. We get the authentication. We should get real time out of the box as well. So we just need these components. Right? Channels and messages should be fairly straightforward. We'll just call it a channel. Why would you say that? That's like the beginning of the it should be it should be straightforward. Keep it simple, and it won't be complicated. Keep it simple, and it won't be complicated. Yeah. Alright. What do we need for a channel? Do we yeah. Yeah. We don't even really need any of this. Right? I don't care when it was created, who it was created by. Just need a name for the channel. That'll be an input. We're going to go to the advanced settings. And because I want this to be URL safe, right, we'll use the slugify option inside the interface, and boom. There's channels. That was really complicated. I I saw Ben was He begins now. Let's let's do, like, the sahash. Oh, egg. Oh, god. We don't have time for this, Brian. Dude God, I'm stressed already, and we're, like, 4 minutes in. Everything. Yep. Alright. So then we've got messages. Alright. So as far as messages, we want when that message was created. So let's just call that timestamp for simplicity. Fun fact. I didn't know you could rename those. I literally didn't that is the first time I've ever seen that. Damn. Yeah. You could change it. You know, like, some people prefer updated at, versus date created or update updated. Do whatever you want with it. Alright. So we've got some messages within the messages. What do we have? We have what do we wanna do? Just text text area. We could go to markdown to be super complicated, but we'll just call it content, text, message. What do you Yeah. I literally I literally don't care. Oh my god. No. I I wouldn't do message because if you're, like, looping through messages, you might use message as the singular, and then, yeah, text feels good. Text. Alright. Let's unhide this just so we could see. My leg's literally doing its nervous shake. I'm so glad this is your series and not mine. Oh my gosh. I'm sweating. Is it is it just the technical difficulties at the start of it? Or or what? No. It's having an hour on the clock. It's having an hour on the clock, Brian. It's the very format. Yeah. No worries. Alright. So we got some messages. We got some channels. What else do we really need here? Oh, we'll come back to threads later. Right? We've already got our users. I'm gonna go ahead and, let's add you as a user, Kevin. Kevin, add example. And I'm not gonna tell you guys what the password is here. Alright. Now let's go into I did actually think reactions would be a nice sorry. That was me interacting with, Ben in the chat. I did actually think reactions would be a nice a nice one as well. It's quite common. Yeah. Cool. It'd be an interesting one to add. Yeah. We'll come back to it for sure. Alright. So we're gonna create a role for users so we can give access to messages for channels, etcetera. We're just gonna go ahead and give all access to this to start with. And the other thing that I'm gonna do, let's give public access to create we'll come back to, like, registering users. Yeah. Yeah. Yeah. No. That's later. Yeah. Okay. Alright. So we've given access. Now we can access any as long as we're in this user role, we'll be able to access channels, messages, all of that. Let's actually start to build something. Right? If I wanted to, I could give Kevin access to this right now. He could log in. Let's just do that. You're just seeing my stress face here. Okay. I just see your stress face. Kevin at example. And Kevin Oh, that is a lowbrow password. Is it lowbrow? Wow. I mean Yeah. So let's go in. I'm gonna just create a couple of channels. Tests, though. Oh, yeah. That's right. Let me just go ahead and give that. Because eventually, you're gonna be interacting through this Nuxt front end. But Yeah. Yeah. Cool. Alright. So we create a couple channels. Right? What do we like, guys? Throw some suggestions in the chat. We'll add a few of these channels. Let's start with Random? Evan is crazy nervous channel. My leg's shaking. General, memes. Okay. Sounds great. Got some memes. Alright. And now as far as messages, right, we got the user. We got the timestamp. We've got when it was updated. In case we change it, we got the text. But the thing that we don't have is how these messages belong to a channel. So inside Directus, we'll just go in. Again, creating these relationships, super easy. I don't even have to touch SQL. We'll go in, create a mini to 1 relationship for this. We'll call it channel, and we'll use our channels collection. And what else do we need? Maybe display template. We'll use name. Great. Cool. And now I could populate a message into a specific channel. Hey, Kev. I'll be nervous. I have the most faith in you while having absolutely no faith in you at the same time. By the way, that user you created for me still doesn't have app access. You might need to go into the user settings and allow that. I thought doing the role would be sufficient, but, oh, I'm not I'm not in that role. I'm not in that role. Users in that role was 0. Yeah. Gotcha. Yep. Try it now. Okay. I'll tell you in a moment. Tell me in a moment. Alright. So we've got the basic logic here. Like, we could go and fetch the data from this API if we wanted to just by going to messages or item slash messages. We're gonna get a forbidden because I'm not logged in, but that's okay. We'll take care of that. Oh, we're 10 minutes in. This? Sorry. No stress, but we're 10 minutes in. That's half my time here. I'm I'm halfway to freedom because I am sweating so hard. Don't sweat, man. Somehow I got logged out. So chaos rains, my friend. Oh my gosh. Glass work. Okay. Alright. I don't know how I got logged out, but okay. So we've got channels. We've got messages. Let's actually do something. I low key hate this, he says. I can still see the chat in the other window, Kevin. So you're not you're not giving me much confidence. I'm so sorry. Alright. Yeah. No worries, man. No worries. Alright. So this is my NUC starter from all the other episodes that we've got. I've switched it up a little bit just so you have a simple plug in for the sake of this live instance because the the other starter that had had a Nuxt module. It's got a lot going on. This is really simple. Right? So we've got a direct us plug in, that is in the plug ins directory inside Nuxt, which will automatically register this. And really simple. Right? Inside Nuxt, we've got a runtime config that we're calling to get the directus URL. And my Nuxt config looks like this. Right? We've got a direct us URL. Right now, that is pointing to, like, a local host, so we do need to switch this over. We'll just go to our URL here. It is switched over. Great. As far as the server token, I don't think we even actually will be using this. I don't think I've got it anywhere else in my config either. Cool. So we'll save that. And now we want to, like, start fetching some data. Right? There's a couple of routes that I already have set up in this, like a login and a register route. Drink a red bar too. I've got all my caffeine. More I've got my caffeine right here. Don't you worry. Give him some more energy drinks, please. I'm gonna go get one. I'll be right back. Woof. Woof. I am actually doing it. Alright. So let's fire up the dev server for Nux here. As far as the stuff that I've already got, like I said, I've got a a login route, a register route that we'll try to mess with. Looks like this. We'll go in and log in, where we can register. Very simple. And then we just have a index page, and then, I had some testing that I was doing here. Alright. So as far as layout, what do we want? You know, a lot of times, I it's like I I love Tailwind. Tailwind UI is totally worth the money, especially for, like, quick prototypes like this. I usually like, if it's just something I'm hacking on personally, I'll start with, like, the application shells. Like, sidebar layout. Right? This feels pretty good for, like, a Slack type of setup. Yeah. That looks good. Yep. So let's just go in. We've got that set to view. Where are we gonna run into issues at? Probably with the icons because I don't have those set, but no matter. Alright. So as far as our pages, let's do what are we gonna do here? Let's just do, like, an app directory. Within that, we'll probably have, like, some channels. Yeah. That makes sense. Just just And then the dynamic route inside of that. Yeah. Yep. So we'll do, like, channel dot view. And then also, what I can do is set up, like, a a parent root for the app. So if I go here inside the pages directory and I do app dot view, I paste this in. Is this for, like, the sidebar? Like, the sidebar will be held in this? Yeah. Exactly. Nice. Alright. So we'll save that. Let's see what we get. That is a major indentation. Yikes. K. So these are Rike's rules. I'm not sure if he's on here, but, alright. He quit out. He hit the wrong keyboard shortcut. That is absolutely what happened. He got arced. That's what our team say when you accidentally, like, quit the whole window. I'm sure he'll be back in a moment. Until then, you got me, stressed as heck. Got my energy drink, waiting for the waiting for the stress to return. This is my brief moment of respite here. That was a major indentation, right? Is it worth the time of pause? Is it worth the time of pause? Did you get arced? I I don't know what happened. Yeah. I guess I did. We're back. Sorry. You don't need to be sorry and you don't get that time back. Oh, yeah. Yeah. It's still running. I don't know what happened, man. I really don't. Weird. All right. So getting some errors here. We get console wise. Home icon. Navigation. Yeah. This is the only bad part. I'm gonna zoom out just a little bit here so I can find Just to actually navigate this, healthscape of tabs. Yeah. Just delete a bunch of these icons. Gosh. They put so many icons in this. Everyone that direct us has a personal choice between Mac and Windows, by the way. So, you know, some folks use use either. I would say with the folks I work with directly, I think we mostly use Max, but that's definitely not the case across the board. Tim is my resident, brain slug. Tim is is my resident Windows user. Oh, let's see. It's not about getting to use Linux. It's about wanting to use Linux. So folks wanna use it, whatever. Most of the stuff we use is in the cloud, so it doesn't really matter. Yeah. Okay. Well, at least we got something now. Right? Alright. We're gonna ditch teams here at the bottom. Where are you? Your teams. Great. Just ditch that list of stuff. Can you zoom back in? I'm like, I need my spectacles to see it over there. Alright. Teams is gone. Right? So now we want to fetch the list of channels here and display those. So how do we do that, Kev? Do you have the Directus SDK in this app? We do have the Directus SDK in this app. Lovely stuff. Well, have you got it set up as, like, a as, like, a plug in, or do we need to do that now? As a Nuxt plug in. So we're gonna Very good. We'll use dollar sign direct us, use Nuxt app. The only thing that I don't have is, like, the auto imports from the SDK. That was one of the nice things about the the other module I had. So we're gonna need to do, what, read items? Yeah. From atdirectus SDK. I think that's it. Speaking of this. What's it complaining about? White space, I think. Yeah. Yeah. These are, again, these are Rikes, ESLint, and Which are very legit for building actually serious things unlike this. They're very, very legit. Right? Alright. So let's do what do we want? We want channels. There's a ref for channels, and then we're going to well, actually, we should just be able to use Nuxt async data. Right? Nuxt data error. We get an error. We're gonna go, wait, use async data. We'll give this a key. Let's just call it channels. And then we just return something from Directus, right? So Oh, look at that interesting code pilot code using old SDK methods. Fascinating. Yeah. Not right. Right? That's a oh, what are we doing there? Auto completion or something. So we'll do read items. We've got channels. Rates. Oh, man. GitHub Copilot is getting on the way this time. You can turn it off if you want. We do only have an hour. We'll see. Alright. So, basically, we're gonna read all the items from channels. We wanna get the fields. We'll just use the wild card. It's basically just name and ID. Does that do it? It should do it. Let's just destructure this and oh. Yeah. Are you are you As as channels. Needs to be channels, and we'll just call this channels. And hopefully And now now usually use channels. Yep. We just go down here. Item in channels. Very nice. And I think we could it is name. The key is name. Yeah. Yeah. Item dot name. We're just gonna swap this for, like, Nuxt link. One of the interesting things, like, Nuxt link will take a h ref or, like, a 2 property. So that's one of the nice things that I like about it just because it's you know, you're used to using h ref on the regular side when you're creating a regular link. Right. Do you wanna do you wanna know what's happened? We've, we've hit the 20 minute mark, and, thank gosh. I don't have to be here like, I can sweat from the audience. At this point, I'm gonna I'm gonna hand off to the amazing Alex who I'm gonna bring in. Hang on. Let let me add him in. Alex, would you, would you like to say hello? Good evening, everyone. Can you hear me alright? I can hear you grand. Lovely. Your your lovely tones. Excellent. I've I've, enjoyed watching you sweat, Jacob. It's been great. Oh, I'm sorry. It's coming it's coming for you too, mate. Yeah. Yeah. Yeah. I'm I'm learning a lot of you right now as well. So Hey, Viv. Hey. We failed to resolve next link. What is that all about? I'm gonna peace out everyone. I'm gonna put you in Alex's very capable hands for 20 minutes, and then, he will be handing off to Matt. Bye for now, folks. Enjoy the rest. Yeah. I thought we were doing, like, Royal Rumble rules. Everybody just hangs on. Be fun. Oh. You just call me now, Brian. I that's alright, my friend. How are you, dude? It's been a while since we've, like like paired up for something. Yeah. Yeah. Yeah. This is this is cool, man. I, always enjoy, seeing all this view code, which I wish I understood better. Yeah. Most of it is is tailwind at this point. But, yeah. So we having an error right now. Right? We're not having permission to access the channels. So we just need to log in. Right? So I'm gonna go to my login form components. Let's redirect to slash app. And we're gonna try to log in to this thing as well. I guess I need to add a user for you. Yep. I have one for Alex. Alright. Oh, don't forget the role. Alright. So I'm just gonna try to log in as Alex now. So we're gonna log in. Oops. Nope. That's not right. It's auth/login. I already used the link on the home page. That would have worked. So we got Alex at example, and the password is super secure. Okay. Alright. So now we can see at least we have our channels up here. Right? That looks good. Yep. Yeah. Nice. Alright. How are you feeling, Alex? Are you nervous as as Kevin? Like, feeling good? I I've got a feeling you got the power of Bry Ross, under the hood there. So Ross. Big. I've got confidence, man. I got confidence. Okay. Right. Alright. So now we need to fix, like, the h refs here. Like, hey. We wanna navigate to one of these channels. Right? Let's just test if this is gonna work. I'm a script at the top guy. Not sure if we've got any other view fans or not. I like seeing what's going on with the JavaScript first, and we're going to use the routes. So we'll do routes. Let's use routes. And I'm just gonna do this. I'm just gonna log this out. Right? I wanna look at routes a params.channel. K. Good. The other thing that we're gonna need to do somewhere in here where's the main dev? Main content. We're gonna plug in this Nux page here. So this should render the child page, for all my nested routes. Yeah. Alex is a boss. So, I'm not sure if that's John Daniels or not, but, I will say, like, if you've used the template CLI tool at all, Alex is is that guy. He's the guy that put that together originally. Yeah. Thanks thanks to directors having good APIs. It was, yeah, difficult, but we we got there, and it's pretty cool right now. Yeah. Alright. So we got the next page. I save. 2 2 what's going on? Oh, yeah. I forgot to fix the login again. Yeah. Apparently. Yeah. You need to persist that. Alright. So what else do we need to do? We need to fix those links. Channels. Where are you? Alright. So this will be channel or item dot name. Item dot name. Yeah. And then we'll do /channels /item.name. Should get it. We'll do a Nuxt link. Oh, duh. It's app dot channels. Nice. So now we have our channels working. Right? What are we gonna do inside the channels? Right? Great call. What do we do inside the channels? We are going to here is this is where we're actually gonna fetch, like, real time data. Right? Yeah. You think? Yeah. Yeah. I'm looking forward to this part, the the, real time. Okay. Alright. So I'm gonna cheat a little bit. We're just gonna go into our documentation. We do have this nice guide, if you haven't checked it out, on real time multi user chat. There's a vanilla JavaScript version. There's React. There's Vue. Let's just take a look at this. Right? Alright. How are we going to authenticate with real time? So we've got, the authentication composable. We've added the real time composable. All of those are in my plug in here. So it should be as simple as, by calling connect. That'd be right. Let's see. Kevin's got a I think it was Kevin that he's got. We've got a subscribe function here. So we're gonna subscribe to the messages, client dot connect. Yep. Let's see here. This is usually where I run into some SSR issues, but I'll we'll see how you manage this one, Brian. Let me show you. SSR equals false. That's That's the solution there? That's the easiest way. Yeah. Yeah. Absolutely. Alright. Alright. So let's just, is this cheating? Yeah. This probably is. Use whatever you have at your disposal. Right? So the subscription client dot subscribe messages, that's correct. Our fields here that we want, we're grabbing the all the fields on the root level. I guess I could zoom in a little bit and make that easier for everybody. Then we have user. So let's just grab all the fields from the user. We've got message of subscription, and then it looks like there's a receive message function that goes along with this. Yeah. You yeah, you might need to filter on the channel as well, but maybe we can do that in the next step. Yeah. So we've got data type equals subscription. This is just like the init. Right? Let's see if we are getting any. No. Yeah. This is gonna be interesting. I should have set up better authentication. So persisted on this. Alright. Do we see anything from, see some more issues from Tailwind? X mark icon. Where are you? Note to self, on the next one, do not import any of the icons from. So this should be, like, we definitely wanna redirect on this. Right? Let's add app. Let's add some page meta. Define oh, actually, it's not a const. We'll just do define page meta. And if we give this a middleware of auth, it should automatically redirect. And then I'm gonna go into where's my login form? I'm gonna make this really easy for myself, Alex. Oh, good idea. And password, super secure. We'll learn a little bit about everything here. Okay. So it's redirected. Sign in. This should take us back to the app, where we could get all the channels. What am I not seeing here? Right. Do we have to do we need to connect to the client, I guess? Let's plug in why we're not receiving Yeah. Client dot connect. Right? Isn't that it? Okay. Yeah. Yeah. So this is on mounted, but we're not using SSR. This should just work. Right? Where are we gonna stick that? On? Oh, what if we just call subscribe? Set up all these functions, forgot to call them. Alright. Client is not defined. Duh. Because it's not the client, it should be directus dot subscribe. So, again, we're gonna grab that directus plugin that we set up from use Nuxt app. It's error data is not defined. Oh, we gotta pass the data to that. Depcription started. Okay. Alright. So what do we wanna do now? Within the channel, we wanna have all of our messages. Right? I'm assuming that will be an array. Sounds good to me. In our received messages Actually, let's just do this to start with, see what type of data we're getting back. Received message. Alright. So now if we go in, I got, like, 35 tabs here, don't I? Don't we all? For apps. For hours. Live. Or I forgot the URL. It's just 100 apps dotdirectus.app. And I've logged myself out again. Let's keep that. I can't help you here, Brian. Okay. Alright. So now how do I split these out? That's the only thing I get hung up on on our half the time. It's like, hey. How do we make this, go split screen? Alright. So couple things should be happening here. Right? We've got our subscription. Let's make this larger. Right. So the subscription has started. We're in the Kevin is crazy crazy nervous. We'll probably have to filter that on our front end anyway, but, testing 1, 2, 3. We'll just add the channel to begin with. And Oh. Cool. So we we see the real time is connecting. We're getting our data. There's the actual messages. Great. Right. We've got the test. We've got the user is null. Probably permissions. Correct. That is correct. Alright. We need to make sure that the user role can access all the users. So right now, it's just by default, like, just a one user. We'll just make it so we can read all the users, and let's send a new one. Test 567. Save. Get our message. Okay. Now do we get the user data? Okay. Cool. That's a lot more user data than we actually wanted. So let's go in and trim that down. We'll just do first name, last name, avatar. We probably want the ID too. And the dot ID. And the first name, not the first name. First name. Not the catch. Good catch. Alright. Cool. And then here, we need to what? We want to see the message. Where's that message at? Alright. Subscription, create data. We want to filter that. Right? What are we gonna call this? For each message of subscription, We need to populate that data. Right? Do do you wanna filter it at the at the WebSocket level or just at the whenever a new message is received? You could do both, I guess. But Yeah. I I'm trying to think of, like, in Slack, you'd probably have, like, some kinda indicator up here that's, like, hey. Yeah. Yeah. Even if a message in a different channel popped up, you would probably filter it out on the the front end. Right? Yeah. On on the specific channel Because you probably have, like, some type of yeah. Alright. So receive message here. What do we does it show the oh, let's get the channel name as well. Query Oh, yeah. That's that'd be a useful one. Dotidchannel.name. So here, if the data type equals subscription. Create. And data event equals create. We will push those messages into our array of messages. And then instead of this, we should be showing a some messages. Alright. What we doing on time? I've totally lost the window here. Yeah. Me too. Me too. It's about quarter to the hour on my clock. Let's just take a look. 22 minutes left. Okay. Alright. Cool. Fun. Fun. Fun. Alright. GitHub Copilot for the win. Do we how confident are we feeling? I can't even see the chat at this point. But Not confident in that one. So we'll do, like, a list for all the message. Yeah. Again, hey. Like, you gotta take it with a grain of salt. Message dot content. You know, I I think this would get better if we had typed this out, but Yeah. Key message dot ID, we should have that for sure. We'll just close that list, see what we get. Alright. Do we have any messages? The other thing is, like, do we fetch the messages for this channel on Initially. Right? On yeah. And, like, do you have you messed with the the real time and, like, the chat stuff at all, Alex, or no? Not not a not as much as I should have, but, this is an interesting one because you might not even need to use the real time in this in this example. Just Hello, everyone. It's the disembodied voice of Kevin here. Hello. I'm watching, I've been screaming into the void in chat. I know this isn't I know this is an illegal move to throw you a bone here, but the init payload comes with the initial items. Up to a 100, and you can set limit minus 1 to get more than a 100. Okay. Peace out. Bye. What was that, Alex? You wanna miss off the clock, mate. Okay. Thanks, Gabe. I think that's probably just, just we you can start logging out the the all all the messages that come through from the website. Right? So it's probably a yeah. So it would be one of the messages received maybe, with the initial with a different type. I don't know if I've seen that in your logs to be fair. But Yeah. Let's back up. Subscription started. Received message. No. Matt's gonna save the day on this. We should know this. I should know this, Bryant. That's alright. So, like, we we'd also do it this way as well. Right? Where we go in and read items. Oh, nope. That's gonna be coming from the SDK. Read items from directus s e k. Let's just call this Oh, I think no. I'm starting to sweat. Thanks, Kevin. Yeah. No worries. God. What am I doing? Constant populate messages equals async. Yeah. There we go. This is wrong. Totally wrong. Oh, no. That's not that bad, actually. Filter channel equals route dot params.channel. Equal route dot value. No. Should just be route dot is it route dot value? Is route reactive? Hello. Hello, everyone. It's me again. It's Kevin again. I was I was having the chat with Matt, and in the nature of chaos, we've decided to Royal Rumble this. So it is my pleasure to introduce mister Matt Minor. Hello, Matt. Hey, everyone. I, will be the most useless person on this call because I'm not Oh, hardly. I'll help you pick the colors. And I was actually thinking, maybe I could just sabotage Bryant here at the end. Just Sabotage. The headlines of the day or something. Do it, man. Oh, no. Don't don't do that. Can you imagine? What do we have? We have we have 15 minutes. No. There's there's 20 left. We got just just 20 minutes left. We're 2 thirds of the way in. No pressure. But it it doesn't look like Slack yet, does it? Where are the messages? Yeah. Alex, is it is this what you did with your 20 minutes, mate? Mate, we were we were work we were working hard. Yeah. We were working hard. All hardly working. Yeah. Yeah. This was where are we stuck? What are we, what are we doing? Just gonna load the initial messages. So so I know from experience that when you I don't know what's up with maybe how you're including real time here, but that init payload that init message does come. It definitely comes with all of the mess with all of the items in the collection, but I have also observed it's not happening here. So I think that would be the easiest fix is, like, why isn't that working? Because then you can just use the real time interface. Right. Why isn't that working Yeah. Kevin? Yeah. Bloody great question, mate. So I log in. We go to Kevin is crazy nervous. We start this subscription. Yeah. But where is the where's the payload? Where's that init payload? So in oh, oh, I think maybe it Oh, is it is it here? Is it is it because you're just subscribing to the single event? If you remove the event from, like, inside of the, if you get rid of that and get every everything over that subscription, does that help? There it is. Data. Oh, okay. Yep. Okay. So just use that. Nice. Okay. There we go. My man. That's what I was gonna say too. But, Kevin I know. Sorry. I felt I felt emanating from your your corner of the ring. How are you doing today, Matt? Is I'm good. I'm doing good. I'm excited to be here. Good. News headlines for today? Facebook and Instagram. No. Struggling. I I sorry. I was too busy working, so I didn't know that. Oh, yeah. Sorry. I was too I, it just came up when I was logging in to work today. So, how's the chat doing? Is anybody Let's see. You you might have to spread that one, Bryant. I think. Yeah. Yeah. You will. You will. Just enjoying the chaos. Yeah. But for a net but for a net, dude, for a net, don't push it into the array. Just replace the array because Oh, okay. There's gonna be nothing in it at the start. Data dot data. Joshua, just enjoying the cast. Honestly, so are we. As I said, this is our first kind of live event of this kind, and I don't know. I'm having a great time. It is chaotic, and we're just making it up. We changed the format. Who bloody cares? Love it. Yeah. Nope. Alright. So now we got some messages. Right? Let's, let's mess with this a bit. Alright. So we're gonna have a div. We're gonna show the avatar, image source, message. Joshua, it's so funny. You're like, yeah. You know, I'm struggling, like, getting this, you know, flow automation thing to work while listening to you in the background. And the funny bit is some of the people who would be there to help you are currently here. So it's like, have fun struggling, but you're struggling on your own for the next, 15. Nah. Jonathan's there. Funny. And you think you're struggling to get, like, 4 people watching you work at the they were all just staring over Brian Schuler right now. I love it, man. Thank you. This is this is this is more fun for me. I think I feel like I go, when I go shopping with my wife and, like, just follow her around the store. That's what I feel like right now. I know value just in the way. Alright. So far. Let's add some padding for these. What else? We have time for the padding. We have time for the padding. Dude. I'm good. God. I'm sweating again. All things, man. All things. You you gotta have time for it. Alright. So we got some messages. Great. Now we need what? We need a form at the bottom of this one. Right? So we got VText. What does this thing do? This is, just text. Okay. Great. We don't need that. So we just got an input. Text input, b model, new message. Right? So we're gonna populate. We can just scrap all this shit. Oh, were we allowed to curse on this one or no? Well, you've done it now. So yeah. Sure. Anything anything goes. Anything goes. Alright. So we got a new message. We're just gonna add a ref for that. We will remodel new message at key up, enter, send message. Yeah. I don't like that, but, let's just wrap it. We'll give it a button. Oh, actually, I forgot I got the Nux UI library included in this. So we should be able to get something nice just by doing new input. Did that work? No. Nope. Did not work. No pressure, Brian. That's the beauty of this. No pressure. Yeah. We've all gone quiet. Just letting you work for a moment. New button. Just add send message. What are we doing now? Nope. Gotta close that guy. Alright. So we got this. Right? We need to add a handler for this. Just click send oh, let me just remove that. Send message. Nice. And then we've got should have an async function for send message. And this is not right at all. Right? So I think I don't know. How are you gonna tap into the because you you've gotta you've got to send it over the subscription, don't you? Yeah. That's that's where I was going back to your wonderful guy. Oh, no. No. No. I think it's fine. No. No. No. Ignore me. It's totally fine. You can do it. You can do it at that level where you've written send message because received message is at that level too. Right? So Actually Actually client.send message. Right? Should be possible or no? We'll find out in a minute. I think there's gonna be a weird scoping thing here. Let's find out. Well, I think if you just use the create item function with the new SDK, that should work. Do that, but you could also send it over you could send it over the WebSocket connection, and then you're just using that one connection use make a make a HTTP request, honestly. Test. Oh, yeah. That's why you don't trust this. Right? Yes. Request. Yeah. Create item. Yeah. Did you get it? Test. Send message. Alright. What do we get? Unexpected error occurred. Good. Good. Good. Helpful. Helpful. Helpful. That's the channel that's the channel name. That should be the ID. Right? Yes. Correct. Oh, did we get the channel ID? No. That's right. Because, it's in the it's coming from the URL. Yeah. But it's That value there is coming from the URL. Yeah. Yeah. The dynamic part. But the channel itself Oh, wait. Right. Right. It's saying here, ding, ding, Alex. Got it. So hang on. I lower my confidence. There you go. Yeah. Yeah. So we gotta get the channel ID. Where do we have the data? Right? Oh, I understand now. I understand what you mean the ID. Got it. Got it. Got it. So I I think you'll need to do that. That initial load. Yeah. I think I think you do. Yeah. Just to fetch that ID. I really don't wanna do that. But Mate, you've got you've got 10 9 minutes. Oh, we got left? You're doing that. We're doing it. Yeah. Alright. So read items, channels, filter ID equals no. That's our ID. What's the channel name? Right? Or Channel name. This will be wrapped. We'll have what equals. Alright. So you can have the channel. There's the ID. Good. And it's an array out, which is a rookie error that I often make. Woah. Yep. That is. Yeah. So this will be How are you doing over there, Matt? Having a good time? Watching all along. Yeah. I'm waiting for us to get to the colors. I think that's where I'll have my most, impact. Well, we've got we've got a whole 8 minutes, and I think we're about to be finished, I guess. So, you you get a pick of the next function now or 2. If I use the, what, If we use async data, and you can do, like, some transforms on this as well. Using data. Channel. Turn up paste. Is this guy out? I think I think Greg's suggestion was the best to just use the channel ID in the in the URL. Probably. 100%. Requests from that. This needs to be here. I like the pretty URLs, though. Alright. I will just refresh. Go to crazy nervous. Okay. So now we've got the channel. Alright. So we got channel. This should just be the channel dot ID. Best. Okay. So we're okay. It seems that, like, the message was sent, unless I'm wrong. Test. Okay. So it did populate the message. We just don't see it show up here. Alright. So what are we doing wrong there? In that payload, shouldn't it also have the the channel ID? Yep. It does. But it does, but it's not showing in the console there. Test. Okay. There's the test. Send messages. Yeah. That's kinda odd. Why is it not showing that in the payload? Alright. Regardless, we should be getting a message back. Right? Reading undefined dot avatar. This is a simple v f. Right? Yes. Message. Just get rid of it. No one needs an image. No one needs an image. We're using Slack compact mode. Ditch it off. Get rid of it. Just saying that's 5 that's 5 and a half minutes. Yeah. No worries, man. Alright. Test. Alright. So first name. Why is this not populating? User but we gotta have the username. Right? It cannot read first name. It's not user. It's user created or whatever you called it. Right? Or did you rename it to user? No. It should be user created. Yeah. Channel messages dotuser.firstname. This is used, like, a v f message. A user? You're trying to get something out of this. Oh, shoot. Blah blah blah. Kevin is crazy nervous. Still crazy nervous. I'm literally rocking back and forward in my chair here by feeling the heat. So, like, the the messages are coming in, but we're just not populating. We missed something. Messages dot value dot push data dot data. Alright. So if we're receiving data, we're getting data. It's it's a it's another array in there. Data is an array. Yeah. Okay. So, we'll just do this. Someone else who has the cute baby in the background, and I'm using a hardware mute, but, I think that's that's me. And they're they're they're hungry. They're not. Sorry. That might be my fault. Oh, do you I haven't heard I haven't heard yours in the background. Okay. Was it yours? Alright. Could be. They came home, but they're not sounding cute. They're not sounding cute. They're sounding angry. Alright. So now what is the Nuxt command? Right? I think there's one where I can let you guys tunnel into this. Right? Nuxt dev tunnel? Where is this guy? That is brand that must be brand new because I didn't know that was there. But it's built into Visual Studio Code. If you bring up your status bar at the bottom, I thought it was baked in now. No way. That is sick. Alright. So does this actually work? There there's the tunnel right there, the Cloudflare URL. Womp womp. Why Why doesn't it work? You better work it out because you got 2 minutes. At this point, I don't know. Do you guys just, do me a favor and then log in to the direct us instance in this firehouse message? I got you. P p m. Yeah. Nux. Dev. Oh, p m p m. It would be, like, running the let's see. I don't know if that's it or not. I'm stressed. Yeah. So there we go. Yeah. That was me. It's Does it work does it work if you type it in here as well? Yes. I I'm logged in as Alex. I'm logged in as Alex. Alright. Send send one more just so we can say something. Alright. Here? Less stressed. Yay. Yeah. Rock on. Some concept. That's basically feature complete. I was shitting. Hit right at the buzzer. Is Let's go. Because this is a winner. Yeah? Yay. Holy heck. My heart rate. I'm I'm gonna need to take a shower. I'm sweating. Oof. Look at that. Yeah. Wow. Stop the timer. Yeah. Leave it for Stop. Yeah. Series a already. I've watched them, man. And don't forget, there's more in your stressing and dressing in 100 hours. A whole season now available on Directus TV with more coming in April. I I look forward to the other episodes where you guys are are it may be not on there. I I've enjoyed this a lot, though. This is fun. It definitely distracted a little bit. Yeah. I did feel like, should this be, like, 1 app in a 100 hours, but with guests? Oh, wow. Oh, so takeaways from this. Right? What did we what did we learn? I'm not coming back to the next one. Spend more time with Patty. More time with that. Dude, I'm sorry we didn't get to the, like, colors. Like, do you wanna what what color are you feeling, Matt? It's the only reason I'm so sorry. Pick the color. I'm looking for the color. I can't think of the hex code for I didn't 466, double f. What do you want? 4466 f f. No. It's 6644 f f. Knew it. Hopefully, Ben's not watching. What is it? Oh, he's watching. 6:6:8. He's watching. 44 f f. Where are we? Is it not updating? Is that already the indigo oh, there it is. Okay. No? Maybe it wasn't on it here. Is that the sidebar? Yeah. Okay. Sidebar. Yeah. There you go. BG you're gonna find me one more time. 6644 f f? Yep. That's the one. That's it. There it is. Subtleship. Boom. Feature comp New product. Ship it. Thanks everyone for joining us. Yeah. I'm not I'm not kidding. We probably won't do another one now. We probably will, but I won't be here. Gosh. Literally, 30 seconds to go. 30 seconds to go. Congratulations, Brian. Yeah. I feel like we started off on the wrong foot with the 5 minutes of technical difficulties. The for the nodes. Selecting 5 minutes of technical difficulties at the the start of this thing. That's what everybody did not see on the front end. We got there. We got there. Now we're now we're ready to code for, like, another 2 hours. And if you're watching this live, this whole episode will be packaged up as a special at the end of season 1 of a 100 and a 100 hours on director's TV. We'll pop that out tomorrow. And then there are new episodes of a 100 apps and a 100 hours coming in April. Amazing. Excellent. Yeah. Well, thanks for joining us, to the circus episode of 100 Hours 100 Hours. I'm your host, Brian Gillispie. Thanks to my special guests, mister Kevin Lewis, mister Avdv, and Awesome. With the crazy colors coming in. Thank you, everyone. Bye for now. Cheers. Bye. Cheers.",[390,391,392,393],"eaf4cd0d-bb4a-4906-a173-d6f103668a6c","653c7587-db32-4f98-8de4-8952ed4d51b5","20dbfd3a-7b8c-4df8-b0b9-8bd71bec82c2","b8d11703-e240-4b61-9c3e-fc41467de84c",[],{"id":172,"number":131,"show":122,"year":173,"episodes":396},[175,176,177,178,179,180,181,182,183,184,185],{"id":161,"slug":398,"vimeo_id":399,"description":400,"tile":401,"length":402,"resources":8,"people":8,"episode_number":131,"published":403,"title":404,"video_transcript_html":405,"video_transcript_text":406,"content":8,"seo":407,"status":130,"episode_people":408,"recommendations":410,"season":411},"crm","936325383","In this intense one-hour challenge, watch as Bryant incredibly builds a full-featured custom CRM from the ground up using Directus. He builds contacts, organizations, deal pipelines, activities, and more.","f6b880a0-5cd2-45ad-beff-3117f0a78581",56,"2024-04-19","Mission: Customer Relationship Manager","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season.\u003C/p>\u003Cp>If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality.\u003C/p>\u003Cp>That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM.\u003C/p>\u003Cp>A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go.\u003C/p>\u003Cp>So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to.\u003C/p>\u003Cp>What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up.\u003C/p>\u003Cp>So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people.\u003C/p>\u003Cp>I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities.\u003C/p>\u003Cp>Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality.\u003C/p>\u003Cp>Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection.\u003C/p>\u003Cp>Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right?\u003C/p>\u003Cp>When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by.\u003C/p>\u003Cp>That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright.\u003C/p>\u003Cp>So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time.\u003C/p>\u003Cp>And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with.\u003C/p>\u003Cp>Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation.\u003C/p>\u003Cp>So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields.\u003C/p>\u003Cp>Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title.\u003C/p>\u003Cp>And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string.\u003C/p>\u003Cp>Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great.\u003C/p>\u003Cp>K. Phone, email. We'll put email above phone. Cool. Looks nice.\u003C/p>\u003Cp>Alright. Let's just take a look. Right? We've got first name. Go ahead.\u003C/p>\u003Cp>Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact.\u003C/p>\u003Cp>These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations.\u003C/p>\u003Cp>So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great.\u003C/p>\u003Cp>Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right?\u003C/p>\u003Cp>Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization?\u003C/p>\u003Cp>You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company.\u003C/p>\u003Cp>So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save.\u003C/p>\u003Cp>So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that?\u003C/p>\u003Cp>Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop.\u003C/p>\u003Cp>So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship.\u003C/p>\u003Cp>And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that.\u003C/p>\u003Cp>So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations.\u003C/p>\u003Cp>Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right?\u003C/p>\u003Cp>On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great.\u003C/p>\u003Cp>So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here.\u003C/p>\u003Cp>Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes.\u003C/p>\u003Cp>I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right?\u003C/p>\u003Cp>So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection.\u003C/p>\u003Cp>Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright.\u003C/p>\u003Cp>So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay.\u003C/p>\u003Cp>So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league.\u003C/p>\u003Cp>We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay.\u003C/p>\u003Cp>So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great.\u003C/p>\u003Cp>Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this.\u003C/p>\u003Cp>So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at.\u003C/p>\u003Cp>Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great.\u003C/p>\u003Cp>Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data.\u003C/p>\u003Cp>So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right?\u003C/p>\u003Cp>We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields.\u003C/p>\u003Cp>But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal.\u003C/p>\u003Cp>So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here.\u003C/p>\u003Cp>We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact.\u003C/p>\u003Cp>The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes.\u003C/p>\u003Cp>We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially.\u003C/p>\u003Cp>Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right?\u003C/p>\u003Cp>Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection.\u003C/p>\u003Cp>Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it.\u003C/p>\u003Cp>And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages.\u003C/p>\u003Cp>Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application.\u003C/p>\u003Cp>So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right?\u003C/p>\u003Cp>Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red.\u003C/p>\u003Cp>This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted?\u003C/p>\u003Cp>Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that.\u003C/p>\u003Cp>Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo.\u003C/p>\u003Cp>Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got?\u003C/p>\u003Cp>Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray.\u003C/p>\u003Cp>Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here.\u003C/p>\u003Cp>So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great.\u003C/p>\u003Cp>If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value.\u003C/p>\u003Cp>So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like.\u003C/p>\u003Cp>Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right?\u003C/p>\u003Cp>So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on.\u003C/p>\u003Cp>I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright.\u003C/p>\u003Cp>Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need.\u003C/p>\u003Cp>Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage.\u003C/p>\u003Cp>That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there?\u003C/p>\u003Cp>1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact.\u003C/p>\u003Cp>Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh.\u003C/p>\u003Cp>Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale.\u003C/p>\u003Cp>Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on.\u003C/p>\u003Cp>Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay.\u003C/p>\u003Cp>Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display.\u003C/p>\u003Cp>So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back.\u003C/p>\u003Cp>We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail.\u003C/p>\u003Cp>That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL.\u003C/p>\u003Cp>Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool.\u003C/p>\u003Cp>And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout.\u003C/p>\u003Cp>Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created.\u003C/p>\u003Cp>I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here?\u003C/p>\u003Cp>Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right?\u003C/p>\u003Cp>So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great.\u003C/p>\u003Cp>And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense.\u003C/p>\u003Cp>We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone.\u003C/p>\u003Cp>One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call.\u003C/p>\u003Cp>The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way.\u003C/p>\u003Cp>Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting.\u003C/p>\u003Cp>And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright.\u003C/p>\u003Cp>So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email?\u003C/p>\u003Cp>Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great.\u003C/p>\u003Cp>Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good.\u003C/p>\u003Cp>Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived.\u003C/p>\u003Cp>What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right?\u003C/p>\u003Cp>Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed.\u003C/p>\u003Cp>Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date.\u003C/p>\u003Cp>Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right?\u003C/p>\u003Cp>So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal.\u003C/p>\u003Cp>Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright.\u003C/p>\u003Cp>So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities.\u003C/p>\u003Cp>We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type.\u003C/p>\u003Cp>Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment.\u003C/p>\u003Cp>But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to.\u003C/p>\u003Cp>Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner.\u003C/p>\u003Cp>Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well.\u003C/p>\u003Cp>Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay.\u003C/p>\u003Cp>Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright.\u003C/p>\u003Cp>Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now.\u003C/p>\u003Cp>I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar.\u003C/p>\u003Cp>Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah.\u003C/p>\u003Cp>This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that.\u003C/p>\u003Cp>And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good.\u003C/p>\u003Cp>Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that.\u003C/p>\u003Cp>And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright.\u003C/p>\u003Cp>Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr.\u003C/p>\u003Cp>Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that.\u003C/p>\u003Cp>And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool.\u003C/p>\u003Cp>Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right?\u003C/p>\u003Cp>If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see.\u003C/p>\u003Cp>Business. Is there a business? There you go. That looks somewhat like a business. We've got activities.\u003C/p>\u003Cp>Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great.\u003C/p>\u003Cp>And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here.\u003C/p>\u003Cp>That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings.\u003C/p>\u003Cp>So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome.\u003C/p>\u003Cp>Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks.\u003C/p>\u003Cp>We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man.\u003C/p>\u003Cp>And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is.\u003C/p>\u003Cp>It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed.\u003C/p>\u003Cp>That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win.\u003C/p>\u003Cp>I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations.\u003C/p>\u003Cp>We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that.\u003C/p>\u003Cp>Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows.\u003C/p>\u003Cp>This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow.\u003C/p>\u003Cp>We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow.\u003C/p>\u003Cp>In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create.\u003C/p>\u003Cp>Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage.\u003C/p>\u003Cp>We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great.\u003C/p>\u003Cp>If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally.\u003C/p>\u003Cp>I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user.\u003C/p>\u003Cp>We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there.\u003C/p>\u003Cp>We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox.\u003C/p>\u003Cp>We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here.\u003C/p>\u003Cp>For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload.\u003C/p>\u003Cp>Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user.\u003C/p>\u003Cp>Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be.\u003C/p>\u003Cp>We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you.\u003C/p>\u003Cp>And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it.\u003C/p>\u003Cp>And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications.\u003C/p>\u003Cp>Send notification. Cool. Send notification. The find_user.id. Cool.\u003C/p>\u003Cp>Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no.\u003C/p>\u003Cp>Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there.\u003C/p>\u003Cp>That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us.\u003C/p>\u003Cp>Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage.\u003C/p>\u003Cp>Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal.\u003C/p>\u003Cp>It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens.\u003C/p>\u003Cp>Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name.\u003C/p>\u003Cp>Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great.\u003C/p>\u003Cp>So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome.\u003C/p>\u003Cp>Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways.\u003C/p>\u003Cp>Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win.\u003C/p>\u003Cp>That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season. If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality. That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM. A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go. So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to. What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up. So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people. I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities. Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality. Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection. Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right? When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by. That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright. So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time. And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with. Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation. So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields. Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title. And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string. Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great. K. Phone, email. We'll put email above phone. Cool. Looks nice. Alright. Let's just take a look. Right? We've got first name. Go ahead. Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact. These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations. So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great. Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right? Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization? You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company. So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save. So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that? Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop. So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship. And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that. So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations. Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right? On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great. So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here. Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes. I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right? So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection. Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright. So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay. So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league. We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay. So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great. Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this. So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at. Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great. Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data. So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right? We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields. But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal. So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here. We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact. The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes. We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially. Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right? Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection. Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it. And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages. Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application. So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right? Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red. This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted? Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that. Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo. Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got? Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray. Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here. So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great. If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value. So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like. Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right? So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on. I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright. Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need. Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage. That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there? 1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact. Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh. Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale. Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on. Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay. Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display. So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back. We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail. That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL. Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool. And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout. Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created. I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here? Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right? So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great. And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense. We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone. One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call. The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way. Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting. And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright. So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email? Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great. Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good. Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived. What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right? Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed. Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date. Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right? So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal. Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright. So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities. We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type. Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment. But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to. Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner. Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well. Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay. Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright. Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now. I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar. Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah. This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that. And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good. Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that. And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright. Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr. Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that. And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool. Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right? If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see. Business. Is there a business? There you go. That looks somewhat like a business. We've got activities. Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great. And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here. That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings. So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome. Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks. We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man. And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is. It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed. That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win. I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations. We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that. Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows. This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow. We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow. In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create. Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage. We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great. If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally. I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user. We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there. We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox. We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here. For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload. Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user. Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be. We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you. And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it. And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications. Send notification. Cool. Send notification. The find_user.id. Cool. Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no. Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there. That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us. Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage. Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal. It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens. Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name. Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great. So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome. Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways. Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win. That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.","592c22ad-1cb2-4d65-9bd7-e0d7c98fe4f2",[409],"e9e66fa8-0650-4e37-ae8b-74755fdd5dca",[],{"id":157,"number":158,"show":122,"year":159,"episodes":412},[161,162,163,164,165,166,167,168,169,170],{"id":162,"slug":414,"vimeo_id":415,"description":416,"tile":417,"length":418,"resources":8,"people":8,"episode_number":158,"published":419,"title":420,"video_transcript_html":421,"video_transcript_text":422,"content":8,"seo":423,"status":130,"episode_people":424,"recommendations":426,"season":427},"equipment-booking-manager","936331509","In a fast and furious hour, Bryant tackles building an equipment booking manager. Watch as he designs and implement a system to manage videography and photography gear, including features for checking items in and out, reserving equipment, and maintaining a comprehensive inventory dashboard.","f1a4f4a6-b473-4d02-a1c6-17474dad5c02",60,"2024-04-26","Mission: Equipment Booking Manager ","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps, 100 hours, where we build some of your favorite apps or publicly fail trying. Today we've got a really neat use case that we're gonna try to build, an equipment booking manager. This sounds really fancy. I've seen this topic come up a few times within our direct us community, and I I thought, hey, let's tackle it today.\u003C/p>\u003Cp>It's perfect. So in all of the research I did, which is basically about 5 minutes before I pressed record on this, I found a comparable solution. It's called Checkroom, c h e q, Room. Looks like a pretty neat solution, but basically it's an internal app that allows you to manage all of your equipment. So equipment, in this case, it looks like they're targeting videographers, photography, you know, maybe studios, anybody who's got expensive camera equipment or video cameras, etcetera.\u003C/p>\u003Cp>But basically, you know, you can check these items in and out, reserve them for a time slot, manages all your inventory, what's the the status of all those, gives you a nice friendly dashboard. So that's what we're gonna be building today. If this is your first time watching 100 Apps 100 Hours, there are 2 rules. Number 1, we have 60 minutes to plan and build, so we're on a bit of a time crunch. There's no more, no less.\u003C/p>\u003Cp>And rule number 2 is, kind of, the anti rule. Use whatever we have at our disposal. So with that let's build our equipment booking manager. I'm gonna start the clock here and away we go. So I'm inside, Figma here or FigJam.\u003C/p>\u003Cp>This is how I like to plan before I build, just kind of quickly whiteboard things. So let's go in and just flesh out the the actual functionality that we're looking for out of this specific app. Right? We wanna track inventory of all of our different items, of all equipment and items. This could be, like, mobile phones or Android devices if we're doing web development for testing, camera equipment, printers, what whatever.\u003C/p>\u003Cp>We wanna be able to reserve equipment equipment on a schedule. We want to be able to track, details about the equipment. Let's say user can reserve the equipment on a schedule. We're gonna be able to track details of track equipment status. I mean, that's that's pretty much the the bare bones for this.\u003C/p>\u003Cp>You know, when are these things scheduled? Is it available for checkout? You know, maybe we wanna account for things like maintenance. Put, like, a big question mark there. Yeah.\u003C/p>\u003Cp>Okay. That that seems pretty good. Let's let's dive in and actually start planning what our data model may look like for something like this. So I'm just going to drag a nice square over here. Let's change this to red to kind of match.\u003C/p>\u003Cp>Alright. So thinking about this and and just talking it through out loud, obviously we have our inventory. This could be called items, could be called equipment. Let's let's go with equipment. That that seems like a a good compromise there.\u003C/p>\u003Cp>Items has a, kind of, a specific meaning inside Directus, which is just the actual individual pieces of content or data. So let's go with equipment, and then we're going to have, users. Alright. So that's gonna come actually from a Directus underscore user collection that Directus is gonna give us out of the box, which again is really nice. We got that authentication tied to that as well.\u003C/p>\u003Cp>And then we're gonna have, what, bookings for each of the this piece of equipment. Right? So we have a relationship here between equipment and bookings. The user creates a booking for a specific piece of equipment. You know, we have a check-in, check out type of functionality.\u003C/p>\u003Cp>It's great. Yeah. And this could be, you know, one of the other things that I saw here on the check room website, where was this? Inventory. One inventory to rule them all.\u003C/p>\u003Cp>You could potentially bundle these things up. Capture the flag. So, you know, maybe some type of checklist. Right? Let's go through and and just start fleshing this out.\u003C/p>\u003Cp>Right? We're about 4 minutes in. We need to actually start building something. So I'm gonna pull up my instance of Directus, so this side by side. We'll zoom in just a little bit.\u003C/p>\u003Cp>And this is just a blank direct assistance that I've spun up here locally, if I can remember the actual password. I think it's real secure. It's probably just password. Great. So we've got a totally blank instance of Directus.\u003C/p>\u003Cp>Let's start building something. Let's start with equipment. Right. We're going to just call this table equipment. Usually I've got a plural at the end of a lot of my tables, but equipments doesn't make a ton of sense to me.\u003C/p>\u003Cp>So, what are we going to have on this? We're going to have a status, like the default status field here. These just make it easy to add some of the most common functionality you'll have. It uses, like, a draft, published, archived. So I'm just gonna leave that blank for now.\u003C/p>\u003Cp>The sort field, do we really need a a sort for our equipment? Probably not. It might be good to track when it was created, so when it was added to the database, who it was created by, and the last time that information was updated. So we'll just roll with that. And let's give this, a name, right?\u003C/p>\u003Cp>This is the name of this piece of equipment, it's just gonna be a string. Really simple. No need to get fancy with it. Let's require a name for it. And next, let's probably add a a serial number.\u003C/p>\u003Cp>We need a unique identifier for this. You know, maybe we want, we end up printing these on labels or QR codes. Serial number would be a good way to track that. And, you know, we can choose to require that as a value, you know, maybe we don't necessarily need to, but we'll add that. What else do we wanna add here?\u003C/p>\u003Cp>We want to add a description. So maybe that's gonna be a wysiwyg editor. You know, this could be a HTML description, so we'll go with that. The type is gonna be text. I could customize my toolbar if I want to limit the options that we've got, but we'll just roll with the the default selections for now.\u003C/p>\u003Cp>We're probably going to have, what, some images of this or at least one image. You know, maybe we've got a gallery, but let's just call this the image. That's gonna be a relational field, Just a mini to 1 to our directus files collection. Make sure we can add an image for it. What else am I forgetting?\u003C/p>\u003Cp>We've got a status field. So, let's use a drop down for this. We'll call it status. The type, we're just gonna store that as a string. And for our choices, right, when we think through the choices, the equipment is either currently available, it is unavailable, you know, maybe it could be like, hey, it's getting serviced or it's in maintenance.\u003C/p>\u003Cp>I'm not sure if that's worthwhile to track or not. Alright. So let's just add a new one. We're gonna call it available. For the value, we can use lower case.\u003C/p>\u003Cp>Let's give a, maybe, a check mark. Hey. This is available. We'll make it green. Then we'll have unavailable.\u003C/p>\u003Cp>I'm sure there's probably a better naming convention here, but this is what we'll roll with. Maybe like a x. Okay. This is unavailable. And, yeah, maybe we do it like a status for maintenance just to differentiate.\u003C/p>\u003Cp>Maintenance. Wrench, maybe. Let's look for a wrench repair. There we go. We got a toolbox.\u003C/p>\u003Cp>That'll work. Okay. And I think yellow or orange is a good color for that. Great. So we've got available, meaning it's currently in inventory.\u003C/p>\u003Cp>Unavailable means it's out, Not in inventory. And then we have maintenance. Don't need to allow any other values. Looks great. Trying to think of what else we actually need for this data model.\u003C/p>\u003Cp>You know, we may have, like, specific details that we wanted to track, those are escaping me at this particular moment. So we've got equipment. Let's go in and add a few pieces of equipment. You know, we'll just start out and say, this is available. We have a, just looking at my desk here, I have an iPad Pro that is BG iPad Pro.\u003C/p>\u003Cp>Just create a serial number. The most amazing iPad you've ever seen. And we'll just go to Google, and it's still an image. Right? IPad Pro.\u003C/p>\u003Cp>Perfect. Where are you? Apple's gonna sue me, I'm sure. Please don't. Alright.\u003C/p>\u003Cp>Swipe an image here. That's great. We've got an iPad Pro. I may want to adjust my view just a little bit. I can't I don't see those nice images.\u003C/p>\u003Cp>I could go in and add my thumbnail for this. Looks great. Cool. We can make this a larger view. We can make it comfortable, and I can see a little more of that.\u003C/p>\u003Cp>Or I could even go straight to cards view here and get, just kind of like a gallery of where we're at. So the subtitle here could be the status, it's available. Great. We could add one more piece of equipment. You know, maybe I've got a, show my age here.\u003C/p>\u003Cp>The Sony a 64100. Sony a 64100. We could call this 1. Great. So we'll find a image of the Sony.\u003C/p>\u003Cp>I think it's the a 64100. Yep. That's the one. Copy image address. We'll just pull that in.\u003C/p>\u003Cp>Great. And, yeah, maybe I've got something like 2 of these cameras. How can I, like, achieve that really quickly? How can I duplicate these inside Directus? Well, what I could do is just go ahead and change the name a little bit, change the serial number, and I could go into the 3 dots up here at the top and click save as copy.\u003C/p>\u003Cp>So now I've got 2 of those specific cameras, maybe one's available, one's not, one's currently checked out, the other isn't. Cool. Alright. So we've got some equipment. What is the next thing that we're going to build?\u003C/p>\u003Cp>Let's take a look at bookings. Right? So how is this going to work? We're going to maintain a schedule of bookings. We're probably going to use our calendar view inside Directus for this, and then we'll be able to check-in and out this equipment, or actually reserve this equipment.\u003C/p>\u003Cp>The check-in, check out part may be, something else a little different. So let's just call this table bookings, makes sense. For the primary key we'll just use an ID. I'll use generated UUID for that. For the status, right, Let's leave status off here.\u003C/p>\u003Cp>We're just gonna check the box for all of the date created, date updated, all these system fields that auto populate when certain actions happen. And now we've got a booking, right, so when is the we we need the times for the booking. So let's do, like, a scheduled from. We use a time stamp format just so we get the time zone value. K.\u003C/p>\u003Cp>Let's make that high second half width. We'll do schedule 2 for that as well. Schedule from, schedule 2. Do we have notes? You know, I'm thinking, like, hey, we may have to have some type of notes.\u003C/p>\u003Cp>We'll use a text area for that. I don't need these to be formatted in a WYSIWYG or markdown. You know, could be potentially nice if you need that level of functionality. Alright. So we've got a booking, we've got schedule from, schedule to.\u003C/p>\u003Cp>We need, obviously, a piece of equipment tied to this booking. So now we're gonna start reaching for our relationships inside Directus, because each booking could only have a single piece of equipment, or could it? You know, we could potentially book a 3 or 4 pieces of equipment, but maybe those would be individual bookings as well. You know, it gets more complicated when you have a kit. Maybe we'll try to explore that.\u003C/p>\u003Cp>But for now, let's just keep it simple. We'll do a mini to 1. Or would it be, yeah, let's do mini to 1 for now. The related collection will be equipment. And we'll go into the relationship here.\u003C/p>\u003Cp>I can see what this setup looks like, but I can go in and also add that inverse relationship. So, I can add equipment to my bookings or add bookings inside my equipment. So I if I pull up a piece of equipment, I can see all the bookings right inside that item detail page that that that particular piece of equipment has had. Alright. Long winded explanation there, Brian.\u003C/p>\u003Cp>Alright. So we've got a schedule from, schedule 2, notes, equipment. Who is this assigned to, or who's pulling this equipment? I again, naming things is is probably the hardest challenge in development. Let's just call this assigned to or bookie.\u003C/p>\u003Cp>Call here bookie. Let's call it assign to. That's fine. So now I can't see my users collection here. Directus users is actually collection.\u003C/p>\u003Cp>So we're just gonna go in and expand that system drop down, pick Directus users. I could control the display template here, so what actually shows up. You know, maybe I want to show the image of the user, the first name, the last name. Great. And save it.\u003C/p>\u003Cp>Alright. So we've got the schedule from, schedule to. We probably need a status on this as well. Right? So who is this booking for?\u003C/p>\u003Cp>Alright. The status, when we think about that, again, let's use a drop down just because we get, like, a rich look at it, now that we have the ability to add icons and colors. But when I think about this, let's say, what's the initial state on a booking? Would be new or maybe unconfirmed is what I'm thinking here. So a new booking comes in, somebody has to verify that that booking is okay, or maybe we could do that automatically via flow.\u003C/p>\u003Cp>But we'll call it unconfirmed. And let's just see if we've got, like, a there we go. We've got a new flow. Fiber new. That's an interesting name for that particular icon.\u003C/p>\u003Cp>What else do we have? Scheduled. Scheduled. K. Time.\u003C/p>\u003Cp>See what we got. Okay. Let's just do, like, blue. That's kind of like a neutral color as far as what's going on. You know, maybe in progress is the next one.\u003C/p>\u003Cp>We'll just create some text and value for that. Settings. Maybe, like, a gear, progress bar, meter, bar. Yeah. There we go.\u003C/p>\u003Cp>That'll work. Alright. We'll mark that as I guess, as green in the state of a booking. Maybe it's red. In progress.\u003C/p>\u003Cp>Maybe completed is the state. Alright. So this is a completed booking. Check mark. I think that's a that'll be a nice icon for this.\u003C/p>\u003Cp>Alright. Completed booking. We'll go to check mark. We'll make that green. Great.\u003C/p>\u003Cp>Okay. So now we've got our status field filled out. We'll hit save, and now we can start looking at at how this is gonna come together. So we've got equipment, we've got our bookings, you know, maybe we look at this on a calendar view. Our layout options here we're gonna adjust this view.\u003C/p>\u003Cp>Each one of these layouts inside Directus has, separate configuration options that are dependent on the specific view. So here maybe we display, let's see. We're gonna display the equipment name. That makes sense to me. And then let's also maybe display the user as well, so who this is assigned to.\u003C/p>\u003Cp>Assign to first name. Assign to so we'll reach into that related collection, and I can display those related values. I thought I had added that. Assign to last name. Oh, I'm just not seeing it there.\u003C/p>\u003Cp>First name, assign to first name. I added it, I just didn't see it. Last name. Okay. Alright.\u003C/p>\u003Cp>The start date field is gonna be scheduled from, scheduled to. 1st day of the week is Sunday, and then we're good. Right? So let's go in and create our first booking. We'll just do this manually.\u003C/p>\u003Cp>And I can actually do default values as well, one of the handy things about when when you're setting up your data model inside Directus. If I go into my status, I've got unconfirmed as a value. If I go to my schema, I can just set the default value to be unconfirmed, and now whenever I create a new booking, the default is unconfirmed. Alright. So we're going to pick a piece of equipment.\u003C/p>\u003Cp>Let's say we want this Sony a 64100. That's gonna be assigned to me. I wanna check this out, for 2 days at near the end of this month and using for a photo shoot. Great. So now I could see that booking on my calendar, and presumably as these bookings come in, you know, I could go in and and see this schedule.\u003C/p>\u003Cp>Right? This is really a bare bones functionality. You know, I I could give access to our team to this and set up some separate permissions, but now, I let's maybe add some automation to manage this booking process. Right? It seems kind of hey, the flow doesn't seem quite right to me.\u003C/p>\u003Cp>So we wouldn't have somebody go in and manually adjust the status for their booking. You know, maybe we can take advantage of Directus flows and and build something to to, account for this. Right? So we'll go inside flows, and let's just let's think about how to manage the bookings. Each one of these specific bookings, has some actions that you're taking against it.\u003C/p>\u003Cp>So to me, if you're used to building, like, inventory systems, you probably got some type of transaction against that inventory of, hey, this was checked in, this was checked out, it was reserved, or it the on hand quantity was reduced. So maybe we do something like that where we track the individual transactions against these bookings of when it was checked out, when it was checked in. Let's do that. So we'll do bookings underscore transactions, and I'll just use UUID again. Just kind of the standard for me.\u003C/p>\u003Cp>Maybe the created on do we really need, alright. We're not gonna update these transactions. So, yeah, if we're created on, I'm just gonna change this to timestamp because that is the timestamp this particular transaction occurred. We'll use created by just so we have the user created that particular transaction, so if that was me checking myself in or somebody else checking in that booking. Alright.\u003C/p>\u003Cp>So at that point we also are gonna have a relationship to our bookings. So let's go ahead and set that up. Here it's going to be a mini to one relationship from booking transactions to bookings, because, a a transaction only has one booking, but a booking could have many different transactions attached to it. So we'll call this the booking, and our related collection is gonna be bookings. And for the display template, let's say that is the equipment name, space, date, schedule from.\u003C/p>\u003Cp>And we do schedule to. And if I wanted to, I can even go in and add the the specific user. So we'll do first name, assign to last name. Perfect. Alright.\u003C/p>\u003Cp>Cool. So now we've got a booking, and I forgot to create the inverse relationship. There's when you're creating that mini to 1, there's a second tab for that, but no fear. We can just go into our bookings and and we can add that from here. So we'll do the one to many field.\u003C/p>\u003Cp>So this is the inverse relationship. We'll call this our transactions. And let's do booking transactions and the foreign key we'll already have, which is the booking. So this is the foreign key for bookings inside transactions. We'll just save that, maybe show a link to it and hit save.\u003C/p>\u003Cp>Alright. So now if we take a look at one of our bookings we can see this transaction table. Alright. The other thing that we're probably gonna need on our booking transactions, and honestly, like, this may be hidden behind the scenes, and I'm just gonna go in and add some icons to this before it drives me crazy. Recovering designer folks.\u003C/p>\u003Cp>Bear with me. Camera. There we go. Looks great. Alright.\u003C/p>\u003Cp>So on our booking transactions, and I may even just nest that, we're gonna need a action type. Alright. So it's a check-in, check out. You know, move to maintenance, maybe, as well. For this, let's try something like radio buttons.\u003C/p>\u003Cp>So this will be the action. The choices that we'll have will be check-in, the value here, check underscore in, Check out. Check out. Maintenance. Move to maintenance.\u003C/p>\u003Cp>Request maintenance. Let's just call it maintenance. I'm getting too fancy with it. Taking too much time when I'm actually maintenance. I'm not even sure if that's the correct spelling.\u003C/p>\u003Cp>That's what we're rolling with. Alright. So now, what have we done? We've got some equipment, we've got bookings we can use to manage that, and then we can attach transactions to those. Right?\u003C/p>\u003Cp>So as far as our underlying data model, this looks pretty cool. Let's work on our our flow. Right? How can we make this easier? So I can imagine, like, if I'm looking up bookings, you know, maybe I want to bookmark this.\u003C/p>\u003Cp>This is the calendar view. So we'll add a calendar icon for that as well. Alright. So when I save a bookmark, that gives me the calendar here. And then maybe I can go in and maybe we just want, like, a list of all the bookings as well.\u003C/p>\u003Cp>This could be a new potential bookmark. Let's go in. We don't wanna update that original bookmark. I go back to bookings. We'll just create a bookmark for this.\u003C/p>\u003Cp>We'll call this list. This is a list of bookings. Great. So one of the other nice things about creating these bookmarks inside Directus is if you have certain filters applied or, you know, even a search query, you could save that search query and create these really nice slices of your data. But as far as my list view, you know, maybe I want to clean this up a little bit.\u003C/p>\u003Cp>My assign to field looks kind of boring, my equipment is not showing. Let's just make this look a little nicer. So I'm going to go back inside my data model and where I have equipment I'm gonna look at my interface and display. So here we have our interface, which is gonna be how it looks inside the detail view. The display is gonna be how it presents on the layout.\u003C/p>\u003Cp>So I could go in and maybe we want to show the, image, And I can expand that a little bit just to get a a thumbnail. And we'll show the name of this, and we'll do the same here. One of the little tricks I can use here is just drop this down, copy raw value, we can also paste it here and get access to the same thing. Great. As far as the assign to, maybe we want to do the same thing.\u003C/p>\u003Cp>Copy for the display. Because this is a user, I could also just show the direct us user. And maybe we wanna show the user in a circle instead of a square. Totally up to you. So there we go.\u003C/p>\u003Cp>This looks a little better. We'll probably have the schedule 2 on there as well. And maybe I wanted to get a little fancy with some conditional formatting for the status. Again, pretty easy to do. We'll just go to status, we'll do display, a couple of different ways I could display this.\u003C/p>\u003Cp>Let's just go with a label, and I'm just going to copy the raw value from these choices. I'm gonna paste it here and then, you know, I could adjust the actual colors, like background and foreground if I need to. But let's just see how this presents. Yeah. That looks that looks definitely a lot better, and nice to have like an icon as well.\u003C/p>\u003Cp>Alright. So now we've cleaned this up a bit. Let's build a flow to check-in and out. So what we'll do, we'll go into our flows. Directus Flows is a great workflow automation tool.\u003C/p>\u003Cp>So I can set up, flows to run on any number of things. This one we're going to use the manual trigger for, which is one that I use often. I really like this. So let's call this the check-in. And do we have, like, an in there's an in out.\u003C/p>\u003Cp>Maybe that's, like, a in out burger, maybe? I don't know. Let's, let's do, like, an arrow forward for check-in. That looks good. And then when we do a checkout flow we can move backwards.\u003C/p>\u003Cp>So I move on to the trigger setup. Again, you've got lots of options here. You've got event hook, you could, so basically when an event happens inside Directus we're going to start this specific flow. I could trigger on webhook requests from other third party services, a schedule, or in this case we're going to run this manually. Alright.\u003C/p>\u003Cp>So this is going to be on our bookings collection. Right? The location is going to be where this displays. So if I want to show it on the collection, like the view, the layout page, I can do collection page only or I can do item page only to show it with in a specific booking. Or if I do both, I can, get get the option to pick from the collection page or I could do it from the detail page as well.\u003C/p>\u003Cp>So great. We'll go in and let's just require confirmation on this specific booking and, you know, let's add some inputs for this. Let's add some notes. Any notes about this booking? So I'm trying to think of what this would be.\u003C/p>\u003Cp>You know, maybe, hey, they showed up late or, you know, something like that. But we're already gonna be picking a booking so we know who this is. Let's give this a shot. And the first time I'm, the first thing I do whenever I'm building flows is basically start with my trigger, and then I'm going to go in and actually trigger my flow. So over here on the right, I can see my flows, I can select this, right?\u003C/p>\u003Cp>If I unselect, I won't be able to check this in. But we'll just say, hey, I'm going to check this in and here's some notes about this. Great. Alright. So now if I go back into my Flows, we'll see a couple things.\u003C/p>\u003Cp>Right? We've got our logs that we're gonna take a look at. We've got our payload. And if I look at the body, we've got the notes, we've got our collection, and we've got our keys. So this is the actual ID of the booking that we selected.\u003C/p>\u003Cp>Great. So when I think through this flow, right, what are we going to do? There's a a couple things we're probably going to want to achieve with this flow. If we go back into our data model just thinking out loud here. Alright.\u003C/p>\u003Cp>We're going to this is getting in the way. Let's just do text. We're going to want to create a booking transaction. We're probably going to update the status, update the status of the booking, and then we're going to update the status of the underlying equipment. Right?\u003C/p>\u003Cp>So that's gonna move from available to unavailable. Alright. We can achieve all of those inside of Flow. Let's get to it. Alright.\u003C/p>\u003Cp>So we are going to create our first operation. Let's call this, create booking, create transaction. Okay. We're gonna create data. So we're going to create an item in the database that's going to be our booking transactions.\u003C/p>\u003Cp>We can leave the permission set to from trigger because we're going to run this flow inside Directus. If this was triggered on a webhook event or something like that, you would probably want to use full access because you're not going to provide that accountability, that direct us user to actually run this flow. But we'll go into our payload and listening through this, we've got a booking. So let's just throw that out there. And then that is going to be coming from our trigger.\u003C/p>\u003Cp>So whenever I want to access data from that trigger event, I have to use this special syntax and just add a dollar sign in front of it. That is the only time I need to use that to access data from this flow. So only in the trigger. And let me just take a look. It could be handy if you take and copy this, into Versus Code or your editor just to make it easier to remember what it was as you're building out these flows.\u003C/p>\u003Cp>So we've got the trigger dot body dot keys. This is an array. We're gonna have to pick off the first value of that array. Alright. So let's run this again.\u003C/p>\u003Cp>Create transaction, and we'll do booking transactions, and paste this in. Trigger dot body dot keys, and we're gonna grab the first item from that array. And again, we're using the mustache syntax here. We can close this, and we're also going to have an action. Alright.\u003C/p>\u003Cp>So if you remember, that action is going to be check-in. And is that all we need? I think that's all we need. Great. Alright.\u003C/p>\u003Cp>So the next thing that we're gonna do, we want to update the status of that booking. So this is a check-in. We are going to update the status of that booking to what? So if we look at our booking, we've got, we're gonna change that to in progress. Right?\u003C/p>\u003Cp>Great. So we'll do update our booking, and that's gonna be update data. So we'll just call that update booking. That's gonna be our bookings collection. Again, we'll use the trigger permissions.\u003C/p>\u003Cp>Okay. So 2 ways I could go about grabbing the ID of the item that we're gonna update. I could use the IDs field to specify the specific IDs, or I could run a query for this. I'm going to lean this way just because we already know what that value looks like. So it's going to be dollar sign trigger dot body dot keys, first item of that array, and I'm going to press enter to make sure we store that.\u003C/p>\u003Cp>I can also go in and edit the raw value here just to see what that looks like. So again, if I'm using this mustache syntax, it's going to dynamically populate that data for me. We do not want to omit events for this. We don't want to trigger any type of, like, infinite loop using that, so always be careful when you're using that. And for our payload here, we're just going to update the status of this booking.\u003C/p>\u003Cp>The status is going to be in underscore progress. And I think I think that's all we're really looking for here, right. Now, we need to update the underlying piece of equipment as well. And if you take a look at our log, though, right, we are not getting that information here. We're only getting the key for the booking.\u003C/p>\u003Cp>So in that case, before we can update the actual piece of equipment this is attached to, we probably need to get the booking so we can find that, equipment that's attached to it. So let's just create a new operation. We're going to call it find booking. We're going to use the read data operation. We're going to find bookings collection.\u003C/p>\u003Cp>And then, again, we're just gonna do the same one, trigger.body.keys, save that. And for our query here, I could pass any of the the standard query parameters that are available. If you're not familiar with those, check out our docs. But what I'm gonna do, I'm gonna pass a fields query, or fields property, and this is gonna be an array. I'm gonna get all the root level fields, that's what this asterisk will do, And then I'm also going to get all of the equipment fields as well.\u003C/p>\u003Cp>So I am drilling through my relationships here. One of my favorite features of Directus is the ability to get and expand those relationships in a single API call. So assuming that goes well, then we are going to update that piece of equipment. Right. Before we do that, let's just stop and go test this out.\u003C/p>\u003Cp>Alright. So we are gonna go here. I'm gonna hit check-in. Here's some notes, and, you know, I could also save these on that transaction. We'll maybe do that in a moment.\u003C/p>\u003Cp>But we can see here the change. We've got a status of in progress. We've got a new transaction here. We can see the time stamp that was created. It was a check-in action.\u003C/p>\u003Cp>Let's go into that booking transactions. We'll add that notes field as well, just so we can store those notes. Great. And let's run into our flows and just see the log. Right?\u003C/p>\u003Cp>What did we get for the booking? Are we getting the data that we want? We can see the equipment dot ID. That's what we wanna update, and we're gonna change that status to available or unavailable because it it's unavailable. Alright.\u003C/p>\u003Cp>So this last link in the chain here will go in and update equipment. And You can see Directus will automatically generate the key for me. If you want to change that, feel free to. Not a big deal. But these keys are how you access the data in subsequent operations.\u003C/p>\u003Cp>Right. So as you can see in just a moment, we're going to go into choose our equipment collection. We'll do from trigger. But as far as the ID's that we're going to update, we're going to use, find booking_orfindbooking.equipment.id. So that is how we're gonna access this.\u003C/p>\u003Cp>And for our payload, we're just gonna change the status. Unavailable. Okay. Great. Cool.\u003C/p>\u003Cp>Alright. So I'm just gonna go in and manually change this. This is, let's call it a scheduled booking, for I'm gonna undo these transactions, unhide those just because I want to delete this one. Alright. So we're starting fresh.\u003C/p>\u003Cp>Everything's available. We've got a booking for our specific camera. I'm gonna go in, select this. I can also go into the item detail page here and do the same thing. And, oh, also forgot to update our notes.\u003C/p>\u003Cp>Right? Let's go ahead and do that as well. The perks of building on the fly. How are we looking time wise? Yeah, good.\u003C/p>\u003Cp>Pretty pretty solid time wise. Alright. So for our transactions here, we're gonna pass the notes, and that will be trigger, dollar sign trigger, dot body dot notes. Okay. Just some standard JSON format there.\u003C/p>\u003Cp>Great. Alright. Moment of truth. Let's see if this is actually gonna work. We'll go in, run check-in, our check-in flow.\u003C/p>\u003Cp>Bryant was late showing up to pick up this camera. Great. We'll hit run flow. Now we can see this is an in progress booking. If we look at our equipment, it shows as unavailable.\u003C/p>\u003Cp>You know, maybe we could potentially, show that a little nicer, maybe with a badge or something. And what else did we update? Right? We've got a transaction, we're storing our note. If we go into that detailed booking, we could see the transaction here.\u003C/p>\u003Cp>Maybe we want to clean this up a little bit for our users because we don't want them to see that UUID. Alright, so again, optimizing for our users. Let's go in and we'll go to the transactions. What do we want to do? We want to do show the timestamp, what the action was, and maybe if there's any notes.\u003C/p>\u003Cp>Oops. Let's do notes. See that? Cool. Okay.\u003C/p>\u003Cp>So now I can see this, 1 minute ago it was a check-in event, Bryant was late showing up to pick up this camera. And I can even go a step further with this, or I should be able to, in the action here for my display, I could show a formatted value of this. So we can show a border, and if this is a check-in, you know, maybe we wanna show, what did we use for this? Maybe, yeah, green will be fine. We could show a checkbox.\u003C/p>\u003Cp>Or, actually, I think we were using, like, a arrow. Right? Arrow. Check-in. And check out of it.\u003C/p>\u003Cp>So if the value equals that, we can actually, I'm getting these wrong, aren't I? We'd probably be checking out the equipment, not checking in. So maybe I need to go in and fix that as well. Arrow. Left.\u003C/p>\u003Cp>Right. There we go. Check-in. Oh, let's use left for that one. Great.\u003C/p>\u003Cp>Okay. Alright. Cool. So now we've got our actions. If I take a look at our transactions, we could see we got a badge for that, and I would probably actually flop these.\u003C/p>\u003Cp>Right? That would be this would be like a, yeah, checkout and not check-in. So we can change this. Right? This is a checkout event.\u003C/p>\u003Cp>And is there anything else we need to? I don't think we need to change any of these other ones. Alright. Cool. So let me go in and just fix this again.\u003C/p>\u003Cp>Alright. I'll delete this record so we don't have any more booking transactions there. Reset this guy to scheduled, and let's try it again. Check out. Late again.\u003C/p>\u003Cp>Run one flow on the selected item in progress. Now we got our checkout. Great. So I could do the same operation just in reverse for, like, a a check-in. So when we are checking this equipment back in, pretty easy to do.\u003C/p>\u003Cp>What other functionality do we need out of this? You know, is there an opportunity to take this one step further? We still got, about 15 minutes here. So what if we were to send some notifications on check-in or, like, if we got a new booking, we want to send some type of alert or or notification to someone. So maybe we reach into our flows for that.\u003C/p>\u003Cp>We'll go in and, you know, we could do a maybe like a a check-in flow as well. Right? So we do all this over again. We are going to do a check-in flow. Isn't that exciting?\u003C/p>\u003Cp>Let's do a notification. Right? New booking notification. Alright. So whenever a new booking occurs, we want to trigger a notification.\u003C/p>\u003Cp>That is going to be using our event hook, and you get 2 types of those event hooks inside Directus, whether that's a filter, which is a blocking. So this is great if I want to run some type of logic and then potentially either adjust the payload of the item that we're saving or, you know, updating, deleting, etcetera, or, you know, if I want to actually cancel that event pending some logic. In this case, we're going to do action non blocking. We don't want to block anything from occurring. Just after a new booking occurs, we are going to run a notification to a user.\u003C/p>\u003Cp>Right? So our scope here is items dot create. That's gonna be on a new booking And this also could be we could condition this down if we wanted to, where we only trigger on unconfirmed bookings. Unconfirmed. Alright.\u003C/p>\u003Cp>So the condition gives us, if, then, or if, else logic. So I can separate my flow into 2 separate paths, right. If the condition passes, we go this way and we run some other operations. If it fails, we do something else. So here's the syntax for this.\u003C/p>\u003Cp>We're just gonna use trigger. I'm breaking my own rules here. Let's just save this as is, and let's actually go create a new booking. And let's do it for the iPad Pro. We'll do the rest of this week.\u003C/p>\u003Cp>That's gonna be assigned to me. And here's some notes. Alright. So we created a new booking. I'm gonna get, a a payload here that I can take a look at, right.\u003C/p>\u003Cp>So the the payload is a little different than what we get from when we manually trigger these. But we can see we've got the equipment, we've got schedule from, assigned to. Great. So only on unconfirmed, what are we going to trigger this on? Basically, the unconfirmed.\u003C/p>\u003Cp>I really don't even think we need a condition here because the default value is unconfirmed. So, yeah, we could we could potentially do something like this. Like status is I think it's not equal to unconfirmed. Let's see if that runs. And then we're going to notify a certain member of the team.\u003C/p>\u003Cp>Right? Let's send a notify Matt. I'm going to pick on Matt on my team. We're going to send Matt a notification. And to do that, we have to have a UUID for that user.\u003C/p>\u003Cp>And I don't actually have a user in this account for Matt. Maybe we'll just use myself. Or we'll just create 1. Matt Minor. Great.\u003C/p>\u003Cp>Alright. We'll pick up the UUID from that specific user. And, you know, part of this flow could be like picking that up dynamically, depending on how you decide the logic of who to notify here. But I could just hard code that value in, hit save or hit enter to record that, and then we're going to add a notification. Right.\u003C/p>\u003Cp>New booking. Please review this booking and confirm. And as far as the collection here, the item, we are going to do what? Let's look at our log here. We got the payload.\u003C/p>\u003Cp>We're gonna do the key. So that's the created booking. And I can link that notification to that specific item. So it looks something like this, where we have trigger dot key. And that should do the trick.\u003C/p>\u003Cp>Alright. One of the other things that we may want to do, let's just check time really quick, we've got about 10 minutes. One of the other things that we may want to do is lock down some of these fields when you're actually editing this data. Right? When I am looking at a booking, I may not want anybody to be able to update the status without going through a specific, like, check-in or check out flow that handles all the logic that we want.\u003C/p>\u003Cp>Or likewise, you know, I I don't want any of my non admin users to be able to change the assignment for this. So there's 2 ways I could do this. If I go through the actual data model, you know, if if the booking status is controlled almost exclusively through Flows, I could go in and I could disable editing for this value. That'll be across the board, basically. Inside the UI nobody can update this value.\u003C/p>\u003Cp>So this is what that looks like. Now if I go into status, it just shows me the read only field, and if I wanted to change the status I would have one of my flows or update this via the API. Right. Here's my notes. I run this flow.\u003C/p>\u003Cp>My status is changed to in progress, and, potentially, I've got some type of, operation here to move this, like my check-in process. Right. But the other ways I could do this are via the Directus access control. So I could go in and create a, you know, let's call it a team member role. They will have app access, they won't have admin access, so they can't adjust the flows, they can't adjust our data model, but we do want them to be able to do certain things, like, view all of our bookings.\u003C/p>\u003Cp>Right. So I can give all access to start with, maybe we start whittling down some of these permissions. There's no need to share it. I don't want them to be able to delete bookings, delete equipment. I do want them to be able to update these fields.\u003C/p>\u003Cp>It could potentially mess with our flows as well, but let's address it. Right. So we're going to use the custom permissions for our bookings, the field permissions oh, I've gotta go in and do no access. Alright. So here's what we could do.\u003C/p>\u003Cp>We want to allow them to update only certain fields. Right. Maybe they can change the equipment in case we need to swap that. They can't change the status. They can change they can't change the assign to.\u003C/p>\u003Cp>They can't adjust the transactions, but they can see them. Sounds great. I can even configure field validation rules, or presets. Like, hey, here's the the default values for those. The only other thing that I'm concerned with here is because we're locking down that status, inside our flow for checkout we're actually using the permissions from the trigger here.\u003C/p>\u003Cp>So we'll just do full access, which basically gives unlimited access to, update these things. Great. Cool. And then, you know, if we go back into access control or team member, maybe we don't want them to update the equipment status either. Alright.\u003C/p>\u003Cp>So we'll give access field permissions. These are the fields that this role can update. They can update the name, serial number, description, image. I can't update the bookings. Status, no.\u003C/p>\u003Cp>Okay. Obviously they can update these. Cool. Alright. So we'll save this And now let's open up an incognito window.\u003C/p>\u003Cp>But first I need to give mister Matt Minor some permissions, or just a login. Matt@example.com. Give it a password. Great. Open up this incognito window.\u003C/p>\u003Cp>Go local. And we'll do matt@example.com. Password. Password. Did I did I get the password wrong?\u003C/p>\u003Cp>Did I even give them a role? So a couple things. I need to give Matt that team member role so he can have app access. And, you know, maybe I typed the password wrong. Password.\u003C/p>\u003Cp>Let's just copy that and make sure. Hit save. Okay. There we go. Alright.\u003C/p>\u003Cp>So now this is what Matt would see when he's logged in. You know, immediately you'd see he doesn't have access to the admin interface. But if he goes within the bookings, he can't update the status, he can't change the assignments, he can't do any of this stuff. But what he can do is run this flow. Here's some notes.\u003C/p>\u003Cp>This is not gonna really change any of this. Let's just zoom back out. Too zoomed in. Alright. So for the iPad Pro, I'm just gonna change this did I lock that down?\u003C/p>\u003Cp>I totally locked that down, didn't I? Let's go here. We'll change status. Go to the disable editing value. I'm just going to remove that just to demonstrate this capability.\u003C/p>\u003Cp>Alright. So now we go to iPad Pro. You can see over here I'm logged in as admin on the right hand side over here. I can change this status to be whatever I want. Hey, this is scheduled.\u003C/p>\u003Cp>I've got full permissions. Matt, when he's logged in, he can't edit any of the specific values. Right? But what he can do, he can still run these flows. And here's my notes about this booking.\u003C/p>\u003Cp>Right. We run the flow. It still updates all of our data, but in a restricted manner. You know, it only runs the logic that we want to run, so we can force Matt down a particular path that we want him to go. Alright.\u003C/p>\u003Cp>So as far as equipment manager, I'm calling this a win. We have got our inventory. Let's look at our list. Right? Track inventory of all of our equipment.\u003C/p>\u003Cp>User reserves equipment on a schedule. We track equipment status. We've got maintenance here. You know, we could easily create a flow modeled after our checkout flow to send this to maintenance, add some notes for it, or I could even just quickly, like, add some maintenance records for this the same way I do my transactions. Right?\u003C/p>\u003Cp>And I could even potentially just add those maintenance notes to this specific transaction. So as far as the equipment booking manager, in 1 hour or less, it's a win. It's good. Is it Checkroom? No, it's not.\u003C/p>\u003Cp>So if you need a more robust tool, make sure you check this one out. What was it again? Check Room? Looks like a pretty cool tool. Give it a free trial.\u003C/p>\u003Cp>I I like building inside Directus because, hey, bookings is is just one part of it. Right? You've got your other workflow. If you're managing your equipment, you're going to have to, plan where you're using that equipment at and things like that. So, inside Directus, in just a couple of tables, in less than an hour you can easily build an internal app to solve some of your problems.\u003C/p>\u003Cp>That's it. That's it for this episode of 100 Apps, 100 Hours. Thanks for joining me. Hope to see you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps, 100 hours, where we build some of your favorite apps or publicly fail trying. Today we've got a really neat use case that we're gonna try to build, an equipment booking manager. This sounds really fancy. I've seen this topic come up a few times within our direct us community, and I I thought, hey, let's tackle it today. It's perfect. So in all of the research I did, which is basically about 5 minutes before I pressed record on this, I found a comparable solution. It's called Checkroom, c h e q, Room. Looks like a pretty neat solution, but basically it's an internal app that allows you to manage all of your equipment. So equipment, in this case, it looks like they're targeting videographers, photography, you know, maybe studios, anybody who's got expensive camera equipment or video cameras, etcetera. But basically, you know, you can check these items in and out, reserve them for a time slot, manages all your inventory, what's the the status of all those, gives you a nice friendly dashboard. So that's what we're gonna be building today. If this is your first time watching 100 Apps 100 Hours, there are 2 rules. Number 1, we have 60 minutes to plan and build, so we're on a bit of a time crunch. There's no more, no less. And rule number 2 is, kind of, the anti rule. Use whatever we have at our disposal. So with that let's build our equipment booking manager. I'm gonna start the clock here and away we go. So I'm inside, Figma here or FigJam. This is how I like to plan before I build, just kind of quickly whiteboard things. So let's go in and just flesh out the the actual functionality that we're looking for out of this specific app. Right? We wanna track inventory of all of our different items, of all equipment and items. This could be, like, mobile phones or Android devices if we're doing web development for testing, camera equipment, printers, what whatever. We wanna be able to reserve equipment equipment on a schedule. We want to be able to track, details about the equipment. Let's say user can reserve the equipment on a schedule. We're gonna be able to track details of track equipment status. I mean, that's that's pretty much the the bare bones for this. You know, when are these things scheduled? Is it available for checkout? You know, maybe we wanna account for things like maintenance. Put, like, a big question mark there. Yeah. Okay. That that seems pretty good. Let's let's dive in and actually start planning what our data model may look like for something like this. So I'm just going to drag a nice square over here. Let's change this to red to kind of match. Alright. So thinking about this and and just talking it through out loud, obviously we have our inventory. This could be called items, could be called equipment. Let's let's go with equipment. That that seems like a a good compromise there. Items has a, kind of, a specific meaning inside Directus, which is just the actual individual pieces of content or data. So let's go with equipment, and then we're going to have, users. Alright. So that's gonna come actually from a Directus underscore user collection that Directus is gonna give us out of the box, which again is really nice. We got that authentication tied to that as well. And then we're gonna have, what, bookings for each of the this piece of equipment. Right? So we have a relationship here between equipment and bookings. The user creates a booking for a specific piece of equipment. You know, we have a check-in, check out type of functionality. It's great. Yeah. And this could be, you know, one of the other things that I saw here on the check room website, where was this? Inventory. One inventory to rule them all. You could potentially bundle these things up. Capture the flag. So, you know, maybe some type of checklist. Right? Let's go through and and just start fleshing this out. Right? We're about 4 minutes in. We need to actually start building something. So I'm gonna pull up my instance of Directus, so this side by side. We'll zoom in just a little bit. And this is just a blank direct assistance that I've spun up here locally, if I can remember the actual password. I think it's real secure. It's probably just password. Great. So we've got a totally blank instance of Directus. Let's start building something. Let's start with equipment. Right. We're going to just call this table equipment. Usually I've got a plural at the end of a lot of my tables, but equipments doesn't make a ton of sense to me. So, what are we going to have on this? We're going to have a status, like the default status field here. These just make it easy to add some of the most common functionality you'll have. It uses, like, a draft, published, archived. So I'm just gonna leave that blank for now. The sort field, do we really need a a sort for our equipment? Probably not. It might be good to track when it was created, so when it was added to the database, who it was created by, and the last time that information was updated. So we'll just roll with that. And let's give this, a name, right? This is the name of this piece of equipment, it's just gonna be a string. Really simple. No need to get fancy with it. Let's require a name for it. And next, let's probably add a a serial number. We need a unique identifier for this. You know, maybe we want, we end up printing these on labels or QR codes. Serial number would be a good way to track that. And, you know, we can choose to require that as a value, you know, maybe we don't necessarily need to, but we'll add that. What else do we wanna add here? We want to add a description. So maybe that's gonna be a wysiwyg editor. You know, this could be a HTML description, so we'll go with that. The type is gonna be text. I could customize my toolbar if I want to limit the options that we've got, but we'll just roll with the the default selections for now. We're probably going to have, what, some images of this or at least one image. You know, maybe we've got a gallery, but let's just call this the image. That's gonna be a relational field, Just a mini to 1 to our directus files collection. Make sure we can add an image for it. What else am I forgetting? We've got a status field. So, let's use a drop down for this. We'll call it status. The type, we're just gonna store that as a string. And for our choices, right, when we think through the choices, the equipment is either currently available, it is unavailable, you know, maybe it could be like, hey, it's getting serviced or it's in maintenance. I'm not sure if that's worthwhile to track or not. Alright. So let's just add a new one. We're gonna call it available. For the value, we can use lower case. Let's give a, maybe, a check mark. Hey. This is available. We'll make it green. Then we'll have unavailable. I'm sure there's probably a better naming convention here, but this is what we'll roll with. Maybe like a x. Okay. This is unavailable. And, yeah, maybe we do it like a status for maintenance just to differentiate. Maintenance. Wrench, maybe. Let's look for a wrench repair. There we go. We got a toolbox. That'll work. Okay. And I think yellow or orange is a good color for that. Great. So we've got available, meaning it's currently in inventory. Unavailable means it's out, Not in inventory. And then we have maintenance. Don't need to allow any other values. Looks great. Trying to think of what else we actually need for this data model. You know, we may have, like, specific details that we wanted to track, those are escaping me at this particular moment. So we've got equipment. Let's go in and add a few pieces of equipment. You know, we'll just start out and say, this is available. We have a, just looking at my desk here, I have an iPad Pro that is BG iPad Pro. Just create a serial number. The most amazing iPad you've ever seen. And we'll just go to Google, and it's still an image. Right? IPad Pro. Perfect. Where are you? Apple's gonna sue me, I'm sure. Please don't. Alright. Swipe an image here. That's great. We've got an iPad Pro. I may want to adjust my view just a little bit. I can't I don't see those nice images. I could go in and add my thumbnail for this. Looks great. Cool. We can make this a larger view. We can make it comfortable, and I can see a little more of that. Or I could even go straight to cards view here and get, just kind of like a gallery of where we're at. So the subtitle here could be the status, it's available. Great. We could add one more piece of equipment. You know, maybe I've got a, show my age here. The Sony a 64100. Sony a 64100. We could call this 1. Great. So we'll find a image of the Sony. I think it's the a 64100. Yep. That's the one. Copy image address. We'll just pull that in. Great. And, yeah, maybe I've got something like 2 of these cameras. How can I, like, achieve that really quickly? How can I duplicate these inside Directus? Well, what I could do is just go ahead and change the name a little bit, change the serial number, and I could go into the 3 dots up here at the top and click save as copy. So now I've got 2 of those specific cameras, maybe one's available, one's not, one's currently checked out, the other isn't. Cool. Alright. So we've got some equipment. What is the next thing that we're going to build? Let's take a look at bookings. Right? So how is this going to work? We're going to maintain a schedule of bookings. We're probably going to use our calendar view inside Directus for this, and then we'll be able to check-in and out this equipment, or actually reserve this equipment. The check-in, check out part may be, something else a little different. So let's just call this table bookings, makes sense. For the primary key we'll just use an ID. I'll use generated UUID for that. For the status, right, Let's leave status off here. We're just gonna check the box for all of the date created, date updated, all these system fields that auto populate when certain actions happen. And now we've got a booking, right, so when is the we we need the times for the booking. So let's do, like, a scheduled from. We use a time stamp format just so we get the time zone value. K. Let's make that high second half width. We'll do schedule 2 for that as well. Schedule from, schedule 2. Do we have notes? You know, I'm thinking, like, hey, we may have to have some type of notes. We'll use a text area for that. I don't need these to be formatted in a WYSIWYG or markdown. You know, could be potentially nice if you need that level of functionality. Alright. So we've got a booking, we've got schedule from, schedule to. We need, obviously, a piece of equipment tied to this booking. So now we're gonna start reaching for our relationships inside Directus, because each booking could only have a single piece of equipment, or could it? You know, we could potentially book a 3 or 4 pieces of equipment, but maybe those would be individual bookings as well. You know, it gets more complicated when you have a kit. Maybe we'll try to explore that. But for now, let's just keep it simple. We'll do a mini to 1. Or would it be, yeah, let's do mini to 1 for now. The related collection will be equipment. And we'll go into the relationship here. I can see what this setup looks like, but I can go in and also add that inverse relationship. So, I can add equipment to my bookings or add bookings inside my equipment. So I if I pull up a piece of equipment, I can see all the bookings right inside that item detail page that that that particular piece of equipment has had. Alright. Long winded explanation there, Brian. Alright. So we've got a schedule from, schedule 2, notes, equipment. Who is this assigned to, or who's pulling this equipment? I again, naming things is is probably the hardest challenge in development. Let's just call this assigned to or bookie. Call here bookie. Let's call it assign to. That's fine. So now I can't see my users collection here. Directus users is actually collection. So we're just gonna go in and expand that system drop down, pick Directus users. I could control the display template here, so what actually shows up. You know, maybe I want to show the image of the user, the first name, the last name. Great. And save it. Alright. So we've got the schedule from, schedule to. We probably need a status on this as well. Right? So who is this booking for? Alright. The status, when we think about that, again, let's use a drop down just because we get, like, a rich look at it, now that we have the ability to add icons and colors. But when I think about this, let's say, what's the initial state on a booking? Would be new or maybe unconfirmed is what I'm thinking here. So a new booking comes in, somebody has to verify that that booking is okay, or maybe we could do that automatically via flow. But we'll call it unconfirmed. And let's just see if we've got, like, a there we go. We've got a new flow. Fiber new. That's an interesting name for that particular icon. What else do we have? Scheduled. Scheduled. K. Time. See what we got. Okay. Let's just do, like, blue. That's kind of like a neutral color as far as what's going on. You know, maybe in progress is the next one. We'll just create some text and value for that. Settings. Maybe, like, a gear, progress bar, meter, bar. Yeah. There we go. That'll work. Alright. We'll mark that as I guess, as green in the state of a booking. Maybe it's red. In progress. Maybe completed is the state. Alright. So this is a completed booking. Check mark. I think that's a that'll be a nice icon for this. Alright. Completed booking. We'll go to check mark. We'll make that green. Great. Okay. So now we've got our status field filled out. We'll hit save, and now we can start looking at at how this is gonna come together. So we've got equipment, we've got our bookings, you know, maybe we look at this on a calendar view. Our layout options here we're gonna adjust this view. Each one of these layouts inside Directus has, separate configuration options that are dependent on the specific view. So here maybe we display, let's see. We're gonna display the equipment name. That makes sense to me. And then let's also maybe display the user as well, so who this is assigned to. Assign to first name. Assign to so we'll reach into that related collection, and I can display those related values. I thought I had added that. Assign to last name. Oh, I'm just not seeing it there. First name, assign to first name. I added it, I just didn't see it. Last name. Okay. Alright. The start date field is gonna be scheduled from, scheduled to. 1st day of the week is Sunday, and then we're good. Right? So let's go in and create our first booking. We'll just do this manually. And I can actually do default values as well, one of the handy things about when when you're setting up your data model inside Directus. If I go into my status, I've got unconfirmed as a value. If I go to my schema, I can just set the default value to be unconfirmed, and now whenever I create a new booking, the default is unconfirmed. Alright. So we're going to pick a piece of equipment. Let's say we want this Sony a 64100. That's gonna be assigned to me. I wanna check this out, for 2 days at near the end of this month and using for a photo shoot. Great. So now I could see that booking on my calendar, and presumably as these bookings come in, you know, I could go in and and see this schedule. Right? This is really a bare bones functionality. You know, I I could give access to our team to this and set up some separate permissions, but now, I let's maybe add some automation to manage this booking process. Right? It seems kind of hey, the flow doesn't seem quite right to me. So we wouldn't have somebody go in and manually adjust the status for their booking. You know, maybe we can take advantage of Directus flows and and build something to to, account for this. Right? So we'll go inside flows, and let's just let's think about how to manage the bookings. Each one of these specific bookings, has some actions that you're taking against it. So to me, if you're used to building, like, inventory systems, you probably got some type of transaction against that inventory of, hey, this was checked in, this was checked out, it was reserved, or it the on hand quantity was reduced. So maybe we do something like that where we track the individual transactions against these bookings of when it was checked out, when it was checked in. Let's do that. So we'll do bookings underscore transactions, and I'll just use UUID again. Just kind of the standard for me. Maybe the created on do we really need, alright. We're not gonna update these transactions. So, yeah, if we're created on, I'm just gonna change this to timestamp because that is the timestamp this particular transaction occurred. We'll use created by just so we have the user created that particular transaction, so if that was me checking myself in or somebody else checking in that booking. Alright. So at that point we also are gonna have a relationship to our bookings. So let's go ahead and set that up. Here it's going to be a mini to one relationship from booking transactions to bookings, because, a a transaction only has one booking, but a booking could have many different transactions attached to it. So we'll call this the booking, and our related collection is gonna be bookings. And for the display template, let's say that is the equipment name, space, date, schedule from. And we do schedule to. And if I wanted to, I can even go in and add the the specific user. So we'll do first name, assign to last name. Perfect. Alright. Cool. So now we've got a booking, and I forgot to create the inverse relationship. There's when you're creating that mini to 1, there's a second tab for that, but no fear. We can just go into our bookings and and we can add that from here. So we'll do the one to many field. So this is the inverse relationship. We'll call this our transactions. And let's do booking transactions and the foreign key we'll already have, which is the booking. So this is the foreign key for bookings inside transactions. We'll just save that, maybe show a link to it and hit save. Alright. So now if we take a look at one of our bookings we can see this transaction table. Alright. The other thing that we're probably gonna need on our booking transactions, and honestly, like, this may be hidden behind the scenes, and I'm just gonna go in and add some icons to this before it drives me crazy. Recovering designer folks. Bear with me. Camera. There we go. Looks great. Alright. So on our booking transactions, and I may even just nest that, we're gonna need a action type. Alright. So it's a check-in, check out. You know, move to maintenance, maybe, as well. For this, let's try something like radio buttons. So this will be the action. The choices that we'll have will be check-in, the value here, check underscore in, Check out. Check out. Maintenance. Move to maintenance. Request maintenance. Let's just call it maintenance. I'm getting too fancy with it. Taking too much time when I'm actually maintenance. I'm not even sure if that's the correct spelling. That's what we're rolling with. Alright. So now, what have we done? We've got some equipment, we've got bookings we can use to manage that, and then we can attach transactions to those. Right? So as far as our underlying data model, this looks pretty cool. Let's work on our our flow. Right? How can we make this easier? So I can imagine, like, if I'm looking up bookings, you know, maybe I want to bookmark this. This is the calendar view. So we'll add a calendar icon for that as well. Alright. So when I save a bookmark, that gives me the calendar here. And then maybe I can go in and maybe we just want, like, a list of all the bookings as well. This could be a new potential bookmark. Let's go in. We don't wanna update that original bookmark. I go back to bookings. We'll just create a bookmark for this. We'll call this list. This is a list of bookings. Great. So one of the other nice things about creating these bookmarks inside Directus is if you have certain filters applied or, you know, even a search query, you could save that search query and create these really nice slices of your data. But as far as my list view, you know, maybe I want to clean this up a little bit. My assign to field looks kind of boring, my equipment is not showing. Let's just make this look a little nicer. So I'm going to go back inside my data model and where I have equipment I'm gonna look at my interface and display. So here we have our interface, which is gonna be how it looks inside the detail view. The display is gonna be how it presents on the layout. So I could go in and maybe we want to show the, image, And I can expand that a little bit just to get a a thumbnail. And we'll show the name of this, and we'll do the same here. One of the little tricks I can use here is just drop this down, copy raw value, we can also paste it here and get access to the same thing. Great. As far as the assign to, maybe we want to do the same thing. Copy for the display. Because this is a user, I could also just show the direct us user. And maybe we wanna show the user in a circle instead of a square. Totally up to you. So there we go. This looks a little better. We'll probably have the schedule 2 on there as well. And maybe I wanted to get a little fancy with some conditional formatting for the status. Again, pretty easy to do. We'll just go to status, we'll do display, a couple of different ways I could display this. Let's just go with a label, and I'm just going to copy the raw value from these choices. I'm gonna paste it here and then, you know, I could adjust the actual colors, like background and foreground if I need to. But let's just see how this presents. Yeah. That looks that looks definitely a lot better, and nice to have like an icon as well. Alright. So now we've cleaned this up a bit. Let's build a flow to check-in and out. So what we'll do, we'll go into our flows. Directus Flows is a great workflow automation tool. So I can set up, flows to run on any number of things. This one we're going to use the manual trigger for, which is one that I use often. I really like this. So let's call this the check-in. And do we have, like, an in there's an in out. Maybe that's, like, a in out burger, maybe? I don't know. Let's, let's do, like, an arrow forward for check-in. That looks good. And then when we do a checkout flow we can move backwards. So I move on to the trigger setup. Again, you've got lots of options here. You've got event hook, you could, so basically when an event happens inside Directus we're going to start this specific flow. I could trigger on webhook requests from other third party services, a schedule, or in this case we're going to run this manually. Alright. So this is going to be on our bookings collection. Right? The location is going to be where this displays. So if I want to show it on the collection, like the view, the layout page, I can do collection page only or I can do item page only to show it with in a specific booking. Or if I do both, I can, get get the option to pick from the collection page or I could do it from the detail page as well. So great. We'll go in and let's just require confirmation on this specific booking and, you know, let's add some inputs for this. Let's add some notes. Any notes about this booking? So I'm trying to think of what this would be. You know, maybe, hey, they showed up late or, you know, something like that. But we're already gonna be picking a booking so we know who this is. Let's give this a shot. And the first time I'm, the first thing I do whenever I'm building flows is basically start with my trigger, and then I'm going to go in and actually trigger my flow. So over here on the right, I can see my flows, I can select this, right? If I unselect, I won't be able to check this in. But we'll just say, hey, I'm going to check this in and here's some notes about this. Great. Alright. So now if I go back into my Flows, we'll see a couple things. Right? We've got our logs that we're gonna take a look at. We've got our payload. And if I look at the body, we've got the notes, we've got our collection, and we've got our keys. So this is the actual ID of the booking that we selected. Great. So when I think through this flow, right, what are we going to do? There's a a couple things we're probably going to want to achieve with this flow. If we go back into our data model just thinking out loud here. Alright. We're going to this is getting in the way. Let's just do text. We're going to want to create a booking transaction. We're probably going to update the status, update the status of the booking, and then we're going to update the status of the underlying equipment. Right? So that's gonna move from available to unavailable. Alright. We can achieve all of those inside of Flow. Let's get to it. Alright. So we are going to create our first operation. Let's call this, create booking, create transaction. Okay. We're gonna create data. So we're going to create an item in the database that's going to be our booking transactions. We can leave the permission set to from trigger because we're going to run this flow inside Directus. If this was triggered on a webhook event or something like that, you would probably want to use full access because you're not going to provide that accountability, that direct us user to actually run this flow. But we'll go into our payload and listening through this, we've got a booking. So let's just throw that out there. And then that is going to be coming from our trigger. So whenever I want to access data from that trigger event, I have to use this special syntax and just add a dollar sign in front of it. That is the only time I need to use that to access data from this flow. So only in the trigger. And let me just take a look. It could be handy if you take and copy this, into Versus Code or your editor just to make it easier to remember what it was as you're building out these flows. So we've got the trigger dot body dot keys. This is an array. We're gonna have to pick off the first value of that array. Alright. So let's run this again. Create transaction, and we'll do booking transactions, and paste this in. Trigger dot body dot keys, and we're gonna grab the first item from that array. And again, we're using the mustache syntax here. We can close this, and we're also going to have an action. Alright. So if you remember, that action is going to be check-in. And is that all we need? I think that's all we need. Great. Alright. So the next thing that we're gonna do, we want to update the status of that booking. So this is a check-in. We are going to update the status of that booking to what? So if we look at our booking, we've got, we're gonna change that to in progress. Right? Great. So we'll do update our booking, and that's gonna be update data. So we'll just call that update booking. That's gonna be our bookings collection. Again, we'll use the trigger permissions. Okay. So 2 ways I could go about grabbing the ID of the item that we're gonna update. I could use the IDs field to specify the specific IDs, or I could run a query for this. I'm going to lean this way just because we already know what that value looks like. So it's going to be dollar sign trigger dot body dot keys, first item of that array, and I'm going to press enter to make sure we store that. I can also go in and edit the raw value here just to see what that looks like. So again, if I'm using this mustache syntax, it's going to dynamically populate that data for me. We do not want to omit events for this. We don't want to trigger any type of, like, infinite loop using that, so always be careful when you're using that. And for our payload here, we're just going to update the status of this booking. The status is going to be in underscore progress. And I think I think that's all we're really looking for here, right. Now, we need to update the underlying piece of equipment as well. And if you take a look at our log, though, right, we are not getting that information here. We're only getting the key for the booking. So in that case, before we can update the actual piece of equipment this is attached to, we probably need to get the booking so we can find that, equipment that's attached to it. So let's just create a new operation. We're going to call it find booking. We're going to use the read data operation. We're going to find bookings collection. And then, again, we're just gonna do the same one, trigger.body.keys, save that. And for our query here, I could pass any of the the standard query parameters that are available. If you're not familiar with those, check out our docs. But what I'm gonna do, I'm gonna pass a fields query, or fields property, and this is gonna be an array. I'm gonna get all the root level fields, that's what this asterisk will do, And then I'm also going to get all of the equipment fields as well. So I am drilling through my relationships here. One of my favorite features of Directus is the ability to get and expand those relationships in a single API call. So assuming that goes well, then we are going to update that piece of equipment. Right. Before we do that, let's just stop and go test this out. Alright. So we are gonna go here. I'm gonna hit check-in. Here's some notes, and, you know, I could also save these on that transaction. We'll maybe do that in a moment. But we can see here the change. We've got a status of in progress. We've got a new transaction here. We can see the time stamp that was created. It was a check-in action. Let's go into that booking transactions. We'll add that notes field as well, just so we can store those notes. Great. And let's run into our flows and just see the log. Right? What did we get for the booking? Are we getting the data that we want? We can see the equipment dot ID. That's what we wanna update, and we're gonna change that status to available or unavailable because it it's unavailable. Alright. So this last link in the chain here will go in and update equipment. And You can see Directus will automatically generate the key for me. If you want to change that, feel free to. Not a big deal. But these keys are how you access the data in subsequent operations. Right. So as you can see in just a moment, we're going to go into choose our equipment collection. We'll do from trigger. But as far as the ID's that we're going to update, we're going to use, find booking_orfindbooking.equipment.id. So that is how we're gonna access this. And for our payload, we're just gonna change the status. Unavailable. Okay. Great. Cool. Alright. So I'm just gonna go in and manually change this. This is, let's call it a scheduled booking, for I'm gonna undo these transactions, unhide those just because I want to delete this one. Alright. So we're starting fresh. Everything's available. We've got a booking for our specific camera. I'm gonna go in, select this. I can also go into the item detail page here and do the same thing. And, oh, also forgot to update our notes. Right? Let's go ahead and do that as well. The perks of building on the fly. How are we looking time wise? Yeah, good. Pretty pretty solid time wise. Alright. So for our transactions here, we're gonna pass the notes, and that will be trigger, dollar sign trigger, dot body dot notes. Okay. Just some standard JSON format there. Great. Alright. Moment of truth. Let's see if this is actually gonna work. We'll go in, run check-in, our check-in flow. Bryant was late showing up to pick up this camera. Great. We'll hit run flow. Now we can see this is an in progress booking. If we look at our equipment, it shows as unavailable. You know, maybe we could potentially, show that a little nicer, maybe with a badge or something. And what else did we update? Right? We've got a transaction, we're storing our note. If we go into that detailed booking, we could see the transaction here. Maybe we want to clean this up a little bit for our users because we don't want them to see that UUID. Alright, so again, optimizing for our users. Let's go in and we'll go to the transactions. What do we want to do? We want to do show the timestamp, what the action was, and maybe if there's any notes. Oops. Let's do notes. See that? Cool. Okay. So now I can see this, 1 minute ago it was a check-in event, Bryant was late showing up to pick up this camera. And I can even go a step further with this, or I should be able to, in the action here for my display, I could show a formatted value of this. So we can show a border, and if this is a check-in, you know, maybe we wanna show, what did we use for this? Maybe, yeah, green will be fine. We could show a checkbox. Or, actually, I think we were using, like, a arrow. Right? Arrow. Check-in. And check out of it. So if the value equals that, we can actually, I'm getting these wrong, aren't I? We'd probably be checking out the equipment, not checking in. So maybe I need to go in and fix that as well. Arrow. Left. Right. There we go. Check-in. Oh, let's use left for that one. Great. Okay. Alright. Cool. So now we've got our actions. If I take a look at our transactions, we could see we got a badge for that, and I would probably actually flop these. Right? That would be this would be like a, yeah, checkout and not check-in. So we can change this. Right? This is a checkout event. And is there anything else we need to? I don't think we need to change any of these other ones. Alright. Cool. So let me go in and just fix this again. Alright. I'll delete this record so we don't have any more booking transactions there. Reset this guy to scheduled, and let's try it again. Check out. Late again. Run one flow on the selected item in progress. Now we got our checkout. Great. So I could do the same operation just in reverse for, like, a a check-in. So when we are checking this equipment back in, pretty easy to do. What other functionality do we need out of this? You know, is there an opportunity to take this one step further? We still got, about 15 minutes here. So what if we were to send some notifications on check-in or, like, if we got a new booking, we want to send some type of alert or or notification to someone. So maybe we reach into our flows for that. We'll go in and, you know, we could do a maybe like a a check-in flow as well. Right? So we do all this over again. We are going to do a check-in flow. Isn't that exciting? Let's do a notification. Right? New booking notification. Alright. So whenever a new booking occurs, we want to trigger a notification. That is going to be using our event hook, and you get 2 types of those event hooks inside Directus, whether that's a filter, which is a blocking. So this is great if I want to run some type of logic and then potentially either adjust the payload of the item that we're saving or, you know, updating, deleting, etcetera, or, you know, if I want to actually cancel that event pending some logic. In this case, we're going to do action non blocking. We don't want to block anything from occurring. Just after a new booking occurs, we are going to run a notification to a user. Right? So our scope here is items dot create. That's gonna be on a new booking And this also could be we could condition this down if we wanted to, where we only trigger on unconfirmed bookings. Unconfirmed. Alright. So the condition gives us, if, then, or if, else logic. So I can separate my flow into 2 separate paths, right. If the condition passes, we go this way and we run some other operations. If it fails, we do something else. So here's the syntax for this. We're just gonna use trigger. I'm breaking my own rules here. Let's just save this as is, and let's actually go create a new booking. And let's do it for the iPad Pro. We'll do the rest of this week. That's gonna be assigned to me. And here's some notes. Alright. So we created a new booking. I'm gonna get, a a payload here that I can take a look at, right. So the the payload is a little different than what we get from when we manually trigger these. But we can see we've got the equipment, we've got schedule from, assigned to. Great. So only on unconfirmed, what are we going to trigger this on? Basically, the unconfirmed. I really don't even think we need a condition here because the default value is unconfirmed. So, yeah, we could we could potentially do something like this. Like status is I think it's not equal to unconfirmed. Let's see if that runs. And then we're going to notify a certain member of the team. Right? Let's send a notify Matt. I'm going to pick on Matt on my team. We're going to send Matt a notification. And to do that, we have to have a UUID for that user. And I don't actually have a user in this account for Matt. Maybe we'll just use myself. Or we'll just create 1. Matt Minor. Great. Alright. We'll pick up the UUID from that specific user. And, you know, part of this flow could be like picking that up dynamically, depending on how you decide the logic of who to notify here. But I could just hard code that value in, hit save or hit enter to record that, and then we're going to add a notification. Right. New booking. Please review this booking and confirm. And as far as the collection here, the item, we are going to do what? Let's look at our log here. We got the payload. We're gonna do the key. So that's the created booking. And I can link that notification to that specific item. So it looks something like this, where we have trigger dot key. And that should do the trick. Alright. One of the other things that we may want to do, let's just check time really quick, we've got about 10 minutes. One of the other things that we may want to do is lock down some of these fields when you're actually editing this data. Right? When I am looking at a booking, I may not want anybody to be able to update the status without going through a specific, like, check-in or check out flow that handles all the logic that we want. Or likewise, you know, I I don't want any of my non admin users to be able to change the assignment for this. So there's 2 ways I could do this. If I go through the actual data model, you know, if if the booking status is controlled almost exclusively through Flows, I could go in and I could disable editing for this value. That'll be across the board, basically. Inside the UI nobody can update this value. So this is what that looks like. Now if I go into status, it just shows me the read only field, and if I wanted to change the status I would have one of my flows or update this via the API. Right. Here's my notes. I run this flow. My status is changed to in progress, and, potentially, I've got some type of, operation here to move this, like my check-in process. Right. But the other ways I could do this are via the Directus access control. So I could go in and create a, you know, let's call it a team member role. They will have app access, they won't have admin access, so they can't adjust the flows, they can't adjust our data model, but we do want them to be able to do certain things, like, view all of our bookings. Right. So I can give all access to start with, maybe we start whittling down some of these permissions. There's no need to share it. I don't want them to be able to delete bookings, delete equipment. I do want them to be able to update these fields. It could potentially mess with our flows as well, but let's address it. Right. So we're going to use the custom permissions for our bookings, the field permissions oh, I've gotta go in and do no access. Alright. So here's what we could do. We want to allow them to update only certain fields. Right. Maybe they can change the equipment in case we need to swap that. They can't change the status. They can change they can't change the assign to. They can't adjust the transactions, but they can see them. Sounds great. I can even configure field validation rules, or presets. Like, hey, here's the the default values for those. The only other thing that I'm concerned with here is because we're locking down that status, inside our flow for checkout we're actually using the permissions from the trigger here. So we'll just do full access, which basically gives unlimited access to, update these things. Great. Cool. And then, you know, if we go back into access control or team member, maybe we don't want them to update the equipment status either. Alright. So we'll give access field permissions. These are the fields that this role can update. They can update the name, serial number, description, image. I can't update the bookings. Status, no. Okay. Obviously they can update these. Cool. Alright. So we'll save this And now let's open up an incognito window. But first I need to give mister Matt Minor some permissions, or just a login. Matt@example.com. Give it a password. Great. Open up this incognito window. Go local. And we'll do matt@example.com. Password. Password. Did I did I get the password wrong? Did I even give them a role? So a couple things. I need to give Matt that team member role so he can have app access. And, you know, maybe I typed the password wrong. Password. Let's just copy that and make sure. Hit save. Okay. There we go. Alright. So now this is what Matt would see when he's logged in. You know, immediately you'd see he doesn't have access to the admin interface. But if he goes within the bookings, he can't update the status, he can't change the assignments, he can't do any of this stuff. But what he can do is run this flow. Here's some notes. This is not gonna really change any of this. Let's just zoom back out. Too zoomed in. Alright. So for the iPad Pro, I'm just gonna change this did I lock that down? I totally locked that down, didn't I? Let's go here. We'll change status. Go to the disable editing value. I'm just going to remove that just to demonstrate this capability. Alright. So now we go to iPad Pro. You can see over here I'm logged in as admin on the right hand side over here. I can change this status to be whatever I want. Hey, this is scheduled. I've got full permissions. Matt, when he's logged in, he can't edit any of the specific values. Right? But what he can do, he can still run these flows. And here's my notes about this booking. Right. We run the flow. It still updates all of our data, but in a restricted manner. You know, it only runs the logic that we want to run, so we can force Matt down a particular path that we want him to go. Alright. So as far as equipment manager, I'm calling this a win. We have got our inventory. Let's look at our list. Right? Track inventory of all of our equipment. User reserves equipment on a schedule. We track equipment status. We've got maintenance here. You know, we could easily create a flow modeled after our checkout flow to send this to maintenance, add some notes for it, or I could even just quickly, like, add some maintenance records for this the same way I do my transactions. Right? And I could even potentially just add those maintenance notes to this specific transaction. So as far as the equipment booking manager, in 1 hour or less, it's a win. It's good. Is it Checkroom? No, it's not. So if you need a more robust tool, make sure you check this one out. What was it again? Check Room? Looks like a pretty cool tool. Give it a free trial. I I like building inside Directus because, hey, bookings is is just one part of it. Right? You've got your other workflow. If you're managing your equipment, you're going to have to, plan where you're using that equipment at and things like that. So, inside Directus, in just a couple of tables, in less than an hour you can easily build an internal app to solve some of your problems. That's it. That's it for this episode of 100 Apps, 100 Hours. Thanks for joining me. Hope to see you on the next one.","d4522b33-cbf4-4d8b-bbce-7a87abb83c9d",[425],"e74a7bc0-b593-44f2-a603-95b7fd47fbce",[],{"id":157,"number":158,"show":122,"year":159,"episodes":428},[161,162,163,164,165,166,167,168,169,170],{"id":163,"slug":430,"vimeo_id":431,"description":432,"tile":433,"length":219,"resources":8,"people":8,"episode_number":143,"published":434,"title":435,"video_transcript_html":436,"video_transcript_text":437,"content":8,"seo":438,"status":130,"episode_people":439,"recommendations":441,"season":442},"virtual-events-platform","936342561","In this 60 min sprint, Bryant builds a virtual events platform that's similar to webinar services like Zoom, Livestorm, and others. Can he build all the functionality like event scheduling, event registration, separate break out rooms, and an integration to Whereby?","d3bfe6e0-cfaf-409d-bf02-010b8c0aa6aa","2024-05-03","Mission: Virtual Events Platform","\u003Cp>Speaker 0: Hi.\u003C/p>\u003Cp>Speaker 1: Welcome back to another episode of 100 apps. 100 hours where we try to build your suggestions or some of your favorite apps in 1 hour or less using Directus or publicly fail, die on the vine trying to build something. If you're new to the show, there are only 2 rules. The first rule is we have 60 minutes to plan and build, no more, no less. The second rule is the anti rule.\u003C/p>\u003Cp>Use whatever you have at your disposal. So then the brakes. Let's dive into today's episode. Right? We're gonna be building an events platform, a virtual events platform.\u003C/p>\u003Cp>Now that sounds to me very very vague, could mean a lot of things to a lot of different folks. But, what kind of products are we talking about? So Zoom comes to mind as far as, like, registering a meeting and performing that meeting. You know, maybe you've got some interactivity like a chat, some registration. You know, you've seen this beautiful, beautiful, beautiful registration page before, I'm I'm sure.\u003C/p>\u003Cp>Not knocking you Zoom, but, like, with all the engineers that you guys have, why is this the best that we get for a registration page? I don't like it. Other softwares that I've looked at, like, DemoHop is an interesting one that, you know, has, like, a science fair where each there's a bunch of different booths, some of the other ones, like, Livestorm. There's a ton of these different webinar platforms every way that you slice it. But what I've discovered and and just in my own experience with these over the years, it's nice to have your own.\u003C/p>\u003Cp>So that's what we're gonna be building today. We're gonna be building our own events platform. Let's run through it. We'll start the clock, pull up 60 minutes, and away we go. Okay.\u003C/p>\u003Cp>So, I am in FigJam here. Let's just kind of mock out our functionality that we like or what we're looking to see out of this particular app. So, as far as our functionality, this is a nice serif font. Maybe we underline that as well. Great.\u003C/p>\u003Cp>Alright. So what do we wanna do? We wanted to set up we can't have all of it underlined. That's not cool. Set up an event.\u003C/p>\u003Cp>You know, allow registrations. You know, have a public landing page, registration page. What else do we want? We wanna have chat during the event. Maybe we wanna have different rooms for the event.\u003C/p>\u003Cp>You know? You think a virtual event, it could just be just, hey. It's a workshop. It's a webinar.\u003C/p>\u003Cp>Speaker 0: Could also be\u003C/p>\u003Cp>Speaker 1: like a week long leap week like we do here at Directus. So maybe we have rooms for that. This this seems like I'm getting way in over my head, so we'll stop there. And then let's let's kinda just flesh out our data model for this a bit. So in my mind, what are we gonna have?\u003C/p>\u003Cp>We're gonna have events. K. And we got that nice purple color. Maybe we'll make this a little bit bigger so we could see it. So we've got events and then we're gonna have, users or attendees.\u003C/p>\u003Cp>You know, that's gonna be our Directus underscore users collection. We'll just pick that up. Directus gives us that out of the box. No reason not to use it. Amazing.\u003C/p>\u003Cp>And then I would imagine there's a junction table here, right, where we have could have multiple events. So these will be, like, registrations, maybe. Naming stuff is always the hardest part of development. So we got registrations, draw some arrows, just connect these, connect these in your mind as well. Great.\u003C/p>\u003Cp>And then what else? You know, if we jive with the rooms, you know, we could have rooms as well. Right? Each room has a different topic, some schedule, etcetera. If we think of, like, our events, they're gonna have a date, start date, duration, or maybe an end date,\u003C/p>\u003Cp>Speaker 0: title, description.\u003C/p>\u003Cp>Speaker 1: What else do we have? Registrations, obviously. Probably an image. Schedule, potentially. Cool.\u003C/p>\u003Cp>Alright. Getting too carried away. Anyway, let's actually start building something. Right? This feels just roughly what we're looking for.\u003C/p>\u003Cp>So let's dive into it. Right? What have I got set up for this? Just gonna close all these other things. I have a Directus instance.\u003C/p>\u003Cp>I've got a Nuxt 3 application, just a starter application set up. It's got a login and register function. I don't even think the register actually works on this thing, but that's what we're working with. Let's go ahead and get logged in to\u003C/p>\u003Cp>Speaker 0: our Directus instance. What\u003C/p>\u003Cp>Speaker 1: did I set the password to? I think it's just password. Real secure. Alright. So we've got a blank instance.\u003C/p>\u003Cp>I've got my FigJam over here on the left. Got Directus on the right, a 125% zoomed in, and let's just dive in. Right? Our first collection, we're going to start here with our events and we'll create a new collection for that. Let's give it a UUID, When it was created, when it was updated, those are all great.\u003C/p>\u003Cp>Let's do status as well for like published events, draft events, completed events. Let's just take a look at that. Right? So we have published, we have draft, we have archived. I could go in and flesh these out further if I wanted to or we could just leave it at that.\u003C/p>\u003Cp>Maybe draft is the default. That's good. Solid. Alright. So here, we're gonna have a name for the event or a title.\u003C/p>\u003Cp>The event title, that's just gonna be a string. A description of this, we want to be the anti zoom here, so we're gonna make this a text field using the WYSIWYG interface for Directus. We're going to call it description. That's our key. And I could adjust our toolbar if I wanted to.\u003C/p>\u003Cp>Maybe I want to add undo and redo. I'm not a big fan of underline, so I'll just remove that. If I need to, I can dive into advanced field creation mode, and specifically with the WYSIWYG editor, you can you've got some formatting options here and options overrides that you can pass to the tiny MCE instance if you need to. Like, you know, maybe you have a special class for buttons or something like that that you want to display inside that WYSIWYG editor. Alright.\u003C/p>\u003Cp>Let's add an image for this. So just a single image. You know, we could potentially have, you know, multiple images, but we'll just keep it simple. That's what we're going to show on a card. As I'm imagining this, on the front end, it's It's just like a running list of events or maybe a grid of cards for the events.\u003C/p>\u003Cp>When you get inside the event, you've got to register. Then once you register, maybe you can see the schedule for the event. Cool. So as far as something like a schedule, right, how should we set this up? If I'm not really sure, like, one of the things that I do a lot, I'm not really sure what this is gonna look like immediately.\u003C/p>\u003Cp>Instead of creating another table, I may use something like the repeater, which is basically a JSON type field. That's what it's going to save as our, inside our SQL database. But let's just call it schedule, and then I could go within this and edit fields to repeat, like a set of fields. So, I don't know. I like a speaker.\u003C/p>\u003Cp>It'll probably be something else. Let's just say the name or label. Okay. We'll do string. That requires a value.\u003C/p>\u003Cp>What's the name of this thing? And the interface would be an input and then we could do something like time, date, time, time. Start time, Start time. Sounds great. That's what we'll roll with.\u003C/p>\u003Cp>Let's make that full width and then we'll use the timestamp format. We'll require value. Maybe we don't require value. May not need to pick a time. Maybe we're just showing scheduling order.\u003C/p>\u003Cp>When does this thing start? Cool. Alright. Sweet. So now we have some events or at least the the start of something for events.\u003C/p>\u003Cp>Let's go in and go back to the data model settings. We're just gonna do registrations and we'll do, like, a date created, user created. Do we even I don't even think we need this. Status. Do we have a status for the registration?\u003C/p>\u003Cp>Maybe it's canceled or something like that. I'm reading too much into it. Right? So here's the date created. Maybe I wanna show this, so I'll click the drop down, show that field.\u003C/p>\u003Cp>Maybe we make it full width, just in case. And for our registrations, we're gonna start digging into our relationships. Right? So when I think of a registration, we've got the event that that registration belongs to, and we've got the user that made the registration. So those are both gonna be basically many to one relationships.\u003C/p>\u003Cp>So we'll go in for the key, we'll add the event, we'll just do events. And for our display template, let's use the title. I can already tell that I forgot to input my start and end date for the event. Amazing. So we'll have to go back and fix that as well, but there's our event.\u003C/p>\u003Cp>Let's go ahead and create our relationship to the users. So we'll say the user that's registered or, you know, this could be like the attendee. Again, naming stuff is hard. Cut me some slack. Alright.\u003C/p>\u003Cp>So I want to pick the directus underscore users collection. I'm just going to dig into the systems drop down here, or system, choose direct us users. I can get fancy with the display on this. Give them the avatar with a nice little thumbnail. We'll do first name, last name, and behind the scenes, this is just mustache syntax.\u003C/p>\u003Cp>So I can edit these raw values if I want, do it that way, or I could just pick from the drop down. That's great. We don't wanna create new users from this. We could potentially select users. Great.\u003C/p>\u003Cp>Okay. So that's our basic data model. Why do we have event? Some type of bug. Alright.\u003C/p>\u003Cp>Regardless, we have events, we have registrations. Let's go in and add our start time for this. How could I forget? So in this case, we wanna save the time zone value, so we're gonna use time stamp. This could be the start date.\u003C/p>\u003Cp>Maybe we have an end date for this or, you know, potentially a duration. I guess you could do either or or both. You know, you could potentially even use flows to calculate that if you wanted to. But this is good. Let's add some icons just to fancy it up a little bit.\u003C/p>\u003Cp>My friend Kevin on the team here will be sweating if you watch me add these icons, but, there's always room for design. Cool. Alright. So we got events. Let's create our first event.\u003C/p>\u003Cp>Right? What are we gonna call this? Let's call it Leap Week number 2 1000. Leap Week 2 1000. We are taking a huge leap forward for Directus.\u003C/p>\u003Cp>Join us as we take a huge leap forward for Directus. Lots of cool stuff. Let's add some formatting to this as well. You know, thing 1, thing 2, thing 3. Cool.\u003C/p>\u003Cp>Alright. And then let's add an image for this. Right? So let's just search Directus Leap Week and see if we can grab, do we have any images? Yeah, we got a couple laying around.\u003C/p>\u003Cp>Cool. So we'll save this image. Is that right? Hope that's the right image. We'll just upload that.\u003C/p>\u003Cp>Everything announced at our 1st Leap Week. And let's set a start date for this. Let's say May 15th is a good date. Maybe this runs 2, 3 days. Yeah.\u003C/p>\u003Cp>Great. So for our schedule, we've got workshop with Bryant. That is the I forget what day we did this now. 17th. Oh, that's it.\u003C/p>\u003Cp>May 17th. Not April 17th, Brent. Alright. Let's say that is at 3. Great.\u003C/p>\u003Cp>Cool. So I'm gonna fill out the schedule workshop with Kevin. Go for 15th at, at 16th at 1 PM. Great. Alright.\u003C/p>\u003Cp>So now we've got our event. Maybe let's just drag this out a little bit, move this around. Okay. One of the other things that I forgot here is probably like a slug. Right?\u003C/p>\u003Cp>I want a nice URL on the front end. So we'll go back into events, we'll just create a new field for it, we're going to use the input, call it slug. The other thing that I'm going to do is go in and make this URL safe, so we could slugify this thing. Now one of the other cool things with the addition of the marketplace, let's look for a slug, Permalink interface to enter URL slugs. Is this what we're looking for?\u003C/p>\u003Cp>No. WP slug interface. Yeah. This is cool. Alright.\u003C/p>\u003Cp>Let's try this out. We'll hit refresh, install this extension. I didn't have this on the first season of 100 apps, 100 hours, But now I can go into our interface and we can choose the slug interface. And our template is going to come from the title and we'll auto generate on create. Let's try that and see what we've got.\u003C/p>\u003Cp>We shrink the title and the slug. Cool. Alright. So now we got a couple settings. We can hit auto generate.\u003C/p>\u003Cp>Generates this nice slug, and it looks a lot like the old days of WordPress. I mean WordPress is still around, obviously, but this is this is nice, I like it. Alright. So we've got events, we've got registrations. What do we do next, right?\u003C/p>\u003Cp>We're gonna look at access control. So this is our settings of who has access to what inside Directus. There's 2 default roles, public and administrator. Let's make the public roles, we want to be able to view the events, We don't want anybody to edit events. And then, for as far as registrations, maybe we want to be able to create registrations and then maybe just read, you know what?\u003C/p>\u003Cp>Let's give full access to that. And, that should be maybe what we want, enough to play around with. Right? So now what are we going to do? Right?\u003C/p>\u003Cp>Let's dive into the front end of this application. Alright. So I'm going to pull this up. Again, I've just got a a Nuxt application here, and let's flesh out a couple of routes. Index dot view.\u003C/p>\u003Cp>We can make this just a list of our events. Alright. So we're gonna go in. I've got Directus configured as a Nuxt plugin here. It's just got some authentication in it.\u003C/p>\u003Cp>We've got real time in case we ever needed it. And then we've just got the REST client. That's the way I prefer it. And we just provide that to the Nuxt application. So we'll call that up like this, const directives equals use Nuxt app, and in this case, we're probably gonna do, like, read items as well.\u003C/p>\u003Cp>So we'll grab that from import items from the atdirectus SDK. And, alright, what's next? Right. So we're gonna do some data. We'll use await, use async data, and we'll do return directus dot request read items.\u003C/p>\u003Cp>And we're reading from the, what, events collection. And then I have a, query for different parameters. Right? We wanna show maybe we only wanna show published events, right, so I could do this. We'll just say status equals published.\u003C/p>\u003Cp>Thank you, GitHub Copilot. Let's see what that gets us. Directors. Cool. Maybe we scrap all of this.\u003C/p>\u003Cp>Would do we even need this to be flexed? Probably not. Alright. Let's see what we got. Data.\u003C/p>\u003Cp>Just wrap this in like a pre tag. See if we get any actual data. Right. So we've set up this filter. We're not getting any data because I'm assuming we did not publish this event.\u003C/p>\u003Cp>So if I go in and publish this, let's see what we have now. Okay. Cool. So we have our event information. What can we do with this, right?\u003C/p>\u003Cp>So I do also have the, like, Nuxt UI library built into this. So if you just go to ui.nuxt, using that, it's got, you know, some components already built in for buttons and cards and things like that. Cool. So without getting too fancy on the UI, let's just mock something up now that we can see we've got some actual data. Right?\u003C/p>\u003Cp>We got a diff. We add some padding. Y 12p y 6. We add a, do we even need an h one? Let's call it h 2.\u003C/p>\u003Cp>This will be font bold. Text, let's make the text large. And let's make it mono, font mono. Alright. So will be events.\u003C/p>\u003Cp>See what we got there. There's our events. And now let's do a grid. Right? So we'll just do a div to wrap this.\u003C/p>\u003Cp>Just grid, maybe grid calls to like a break point, maybe we set the gap to 8, and then we're gonna do a v 4. Right? So, for our events, let's just use this card. I think there's a card component in here. I feel like a nice card component is ucard.\u003C/p>\u003Cp>We've got, like, a header and a footer, or we could just throw it in the middle. So let's do that. Right. V 4. We're gonna do events and data.\u003C/p>\u003Cp>You know? And, usually, I like to destructure those.\u003C/p>\u003Cp>Speaker 0: Yeah. Yeah. Maybe we call it events.\u003C/p>\u003Cp>Speaker 1: And for the key here, we'll use event ID. That way, we don't have to, use, like, an index or something like that, and then we'll actually do this. Alright. So is there actual I think there's, like, a text component here, so let's just do, like, an h three. Maybe we just take the auto completion here from, GitHub Copilot.\u003C/p>\u003Cp>We'll do a little Nuxt image to optimize these images. Works very well with Directus. There's a provider for it. Then we've got the event dot image. We use the event title.\u003C/p>\u003Cp>That seems pretty good. Let's see what we come up with here. Sweet. Except for our description. Right?\u003C/p>\u003Cp>This kind of sucks because it is not what we want. So in this case, we'll do div. Let's do v h t m l event dot description, and remove that. Great. It has no formatting.\u003C/p>\u003Cp>Maybe we've got, do I have to tell when? We got pros class. Yeah. There we go. Alright.\u003C/p>\u003Cp>So we've got that, and let's add a link for this. Right? Maybe we add a little spacing here, n t 4. Maybe we make that add a little spacing here. MT2, MT8.\u003C/p>\u003Cp>It's great. Cool. So now we've got an event looking really nice. You know, I could duplicate this event if I wanted to. Let's just see what this looks like with a couple of more events.\u003C/p>\u003Cp>We're going to call this Leap Week 3000. Flush this out. And what I'm going to do here, we're just going to open this image up. I'm going to get really fancy on this, guys. And we'll just mark this up.\u003C/p>\u003Cp>Scroll this out. 3000. Oh, so fancy. Alright. For Leap Week 3,000, we're going to close this out, upload a new photo, we can see that's Leap Week 3,000.\u003C/p>\u003Cp>And then what I'm going to do here is click Save and Copy from the 3 dot menu button and now I've got 2 events. We've got Leap Week 2000, Leap Week 3000. There we go. Amazing. Okay.\u003C/p>\u003Cp>Alright. So now we need a route for our actual event. Right? So let's set this up. Let's actually add a folder for it just in case we have a couple of other routes within that.\u003C/p>\u003Cp>And then for our events, we're gonna have a slug dot view. So that's just gonna be a dynamic route. We'll view I think it's init. View init. What does that give me?\u003C/p>\u003Cp>That's not the syntax that I like. It'll do. Trying to make it fancy. I need to get my snippets set back up. I had to scrap Versus code.\u003C/p>\u003Cp>Lost all my snippets. Alright. So we got a slug. We're gonna use the route here. So we've got route equals use routes.\u003C/p>\u003Cp>Cool. And then, on the individual page, right, we want to call the specific data. And, you know, maybe we don't even need this full description on our index page, so we could, you know, potentially leave that off. Maybe we just make it the event start date, but we may wanna use that in here. And let's use just, like, a date function.\u003C/p>\u003Cp>I think it's format from date f n's. Formats\u003C/p>\u003Cp>Speaker 0: oh,\u003C/p>\u003Cp>Speaker 1: not currency. I'm gonna do format. What we got? DDD, month month, DD. Where does that get us?\u003C/p>\u003Cp>Doesn't get you anywhere because it breaks it. Forgot the year year. I guess it's all lowercase. And then okay. Yeah.\u003C/p>\u003Cp>Still still got a ways to go memory wise. So date f and s, just a handy library. We got format. What's the date? What are we looking for?\u003C/p>\u003Cp>Like, month is gonna be m m m. Like that one. So we'll do m m m. And what if we do, like, the ordinal of the dates, day of the month,\u003C/p>\u003Cp>Speaker 0: d o?\u003C/p>\u003Cp>Speaker 1: That's what we want. Now we're looking nice. Okay. And then we wanna do the time. Right?\u003C/p>\u003Cp>So maybe we show the time. Local day of the week, AM PM. What's this gonna\u003C/p>\u003Cp>Speaker 0: be? H. What minute? M\u003C/p>\u003Cp>Speaker 1: m m. And then we've got AM, PM, AAA. Wasting too much time on this. Okay. Alright.\u003C/p>\u003Cp>So we got that. Let's go in and just wrap this card in a NuxLink, or at least the content of the card. We'll do 2. And let's make this dynamic. That'll be /events/ event.slug.\u003C/p>\u003Cp>Uh-oh. Oh, forgot to close that. Cool. Alright. So now we've got the links set up, so we can click on these and and move to the route.\u003C/p>\u003Cp>We don't really have anything inside our route though. Right? So I could take this function here, just copy that in, and here we're still going to be reading items. Right? I'm going to call this event, in addition to status.\u003C/p>\u003Cp>I guess we can set this up where if you call it directly, even if it's a draft, maybe you wanna see it. It. So let's just do this. We'll say slug equals route dot params dot slug. That should get us the event that we want.\u003C/p>\u003Cp>And then, the actual event here, we're gonna do, like, a transform function. The nice little part that's baked into the use async data call, that composable inside Nuxt, where basically this is gonna return an array. I could transform this and just pick up the first item of this array. And now if we do this, just do pre class. Again, I always like to do this just to see what data I'm getting back.\u003C/p>\u003Cp>And now we've got just the specific data object for this item. Alright. What's next? Right? We're gonna dive into building this actual page.\u003C/p>\u003Cp>Right? So let's give it a nice h one tag. We've got font bold, font mono, text let's make this giant. We're gonna do the event dot title. Okay.\u003C/p>\u003Cp>Leap week 3,000. What else do we have? If I dig into the clipboard, I've got that event description. We can make this pros large. We wrap that.\u003C/p>\u003Cp>Let's move down. We'll do another div. Throw this into an image, event\u003C/p>\u003Cp>Speaker 0: dot image,\u003C/p>\u003Cp>Speaker 1: class w 96. Where's our actual image at? Okay. There's our image. We could flex these 2, add a little bit of padding.\u003C/p>\u003Cp>I mean, this is not gonna win any design awards, but, hey, it'll be alright. We'll just take these items from the first one. Let's put this below. Do we have our dates? Oh, gotta actually import that function, don't we?\u003C/p>\u003Cp>Import format from datefns functions, I guess. Alright. So we got our date. Add a little margin to the top. Looking great.\u003C/p>\u003Cp>And then what if we wanted to show that schedule? Right? So, maybe we drop down over here on the left. We have a list. And within the list, take a little GitHub completion.\u003C/p>\u003Cp>That's not really what we're looking for. It's gonna be the item inside the event dot schedule. For the key, we used you the item dot label. And then we have yeah. I was wasting more time using the GitHub suggestions.\u003C/p>\u003Cp>Item dot label. Item. We'll just steal this format, and can we do this as well? Dash hmmaa. What does that give us?\u003C/p>\u003Cp>Does that get us where we want? Okay. So now we can see the schedule. We can drop a little schedule class. List.\u003C/p>\u003Cp>List inside\u003C/p>\u003Cp>Speaker 0: list disk. I guess,\u003C/p>\u003Cp>Speaker 1: is that the\u003C/p>\u003Cp>Speaker 0: list item? Why are we why does that\u003C/p>\u003Cp>Speaker 1: look weird? Not sure. Not gonna bother with it. Let's do font bold, font mono, empty 8. Okay.\u003C/p>\u003Cp>Cool. What else we got? So we got the schedule right. Good. We got a page.\u003C/p>\u003Cp>We could go back to our events. Yeah. Maybe we add a link for that up here somewhere in the top. Next link to equals slash slash I think it's\u003C/p>\u003Cp>Speaker 0: just slash.\u003C/p>\u003Cp>Speaker 1: Back to main list of events. Alright. Cool. We got all this going on, we could go into Leap Week 3000, we could go to Leap Week 2000. How are we doing on time?\u003C/p>\u003Cp>We are cruising on time. Alright, so how do we set up registration? Let's go in and what we're going to\u003C/p>\u003Cp>Speaker 0: do, we're going to need to\u003C/p>\u003Cp>Speaker 1: basically authenticate the user first. So we've got to have a user to register. And I am going to cheat here and use, like, a SSO, like, register with GitHub. I've used this in a past project. So I've just got some code over here.\u003C/p>\u003Cp>We're gonna go in and inside my env or my Docker Compose file, I would set this up. I'm missing the client secret and client ID from GitHub, so go inside my GitHub account. We're gonna go to developers, we'll go to applications, is it authorize no. Developer settings, OAuth apps, we're going to create a new OAuth app. We'll call it Events Platform.\u003C/p>\u003Cp>Super secret. The homepage URL will be HTTPS. We'll just use Directus. Do we need we need this callback. And just from past experience, the structure for this is gonna be my directus instance slash auth /login/provider.\u003C/p>\u003Cp>So we're using GitHub, that's gonna be the provider. We're gonna use that callback. Great. And then I can get my client ID. I can get my client secret.\u003C/p>\u003Cp>We'll just have to use a password, log in, get the secret, they'll steal my secrets, bro. Okay. And then the last piece of the puzzle here is we need a default role for that, for that specific user. So what we're gonna do, we'll go into Directus. Let's create a new role for attendee.\u003C/p>\u003Cp>And we're not going to give them app access, we're just going to save it. I'm going to pick up the actual ID for this role, I can see it over here. That's the primary key. We'll paste that in there. I'm going to save that, and then I'm going to stop this container, Docker Compose up one more time, and the effect I should get if I load this again, now I can see this login with GitHub function.\u003C/p>\u003Cp>So I could actually log in with GitHub, and let's see. Oh, yeah. I'd have to actually do my username. Maybe we swap this out. Alright.\u003C/p>\u003Cp>We get a local host, 8055. Log in. Admin, example, password. Alright. And then I log out here.\u003C/p>\u003Cp>We could test this out. But I could also do this on the front end as well. Right? So let's go back to our page. Alright.\u003C/p>\u003Cp>So I'm gonna stick a big registration button over here somewhere, and I think I've got one in register. Is it the register form or the login form? Do I have it? Yeah. There we go.\u003C/p>\u003Cp>I got this button that I've used previously. We're gonna go to our event page. We'll save this. Uncomment this button. I don't really need the divider label.\u003C/p>\u003Cp>Alright. Signing with GitHub to register. Cool. And here you can see I've got a redirect, just basically to a different page. We're going to make this size excel, giant button, just add a class to it, give it a little more padding.\u003C/p>\u003Cp>Duplicate Attribute. Oh, MT 12. We don't want to duplicate that class. Alright. So what is this going to do?\u003C/p>\u003Cp>This should ask me to log in once accessed to my GitHub account. We authorize, and now I've got, like, the index page. So if I go here, if I actually if I signed in, right, probably getting some errors here because we did not give permissions to those specific accounts or that specific role. Right? So for our attendee role, we want them to view all the events, we want them to create registrations, maybe they can view the registrations where the user equals current underscore user.\u003C/p>\u003Cp>Alright. We're gonna have to run like hell here. Apologies for the curse words. Right? So okay.\u003C/p>\u003Cp>If you I look at my Nuxt state. I've got a use state composable that's actually storing our user data. So I should be able to see that user data. I've got the ID of the user. Do I have any other information about it?\u003C/p>\u003Cp>Maybe that's really all I need. Right? So let's try this. We'll do const user equals use state, user. Will that give us access to the user?\u003C/p>\u003Cp>And if we have a user, we can hide this button And maybe if if no user. Okay. And then maybe we add another button. Register. And on this one, we're gonna add a click handler.\u003C/p>\u003Cp>It's gonna be register for event. Okay. How's that looking? V f user. Cool.\u003C/p>\u003Cp>And now we need a function for that. Right? This is gonna be an async function because we're gonna call directus. We are going to import, we need the create item. Okay.\u003C/p>\u003Cp>Async function, register for events. We're gonna call that. K. We'll do a try. Await direct us request registrations event dot ID.\u003C/p>\u003Cp>That's gonna be an actual reactive value, so we'll just add on that. We can add that around the user too just to be safe. And cool. What else did we do? Response.\u003C/p>\u003Cp>You know, I could just console log this response just to verify. Alright. So let's take a look. If I clear out my network request, we hit register, We've got registrations. Cool.\u003C/p>\u003Cp>Great. So now we're registered for this event. If I open my Directus Instance back up where are you, mister Directus Instance? We have registered for this event. I could see this specific user here.\u003C/p>\u003Cp>Maybe I wanted to send an email for this. Let's see how we're doing on time. Roughly 20 minutes remaining. Oh, we're cruising. So let's send an email event for the registrations.\u003C/p>\u003Cp>Send registration email. Registration email. Great. We'll go in. Event hook, so anytime an event happens, in this case we want the action, so after this action happens, we're not gonna block the thread.\u003C/p>\u003Cp>Anytime the\u003C/p>\u003Cp>Speaker 0: new registration\u003C/p>\u003Cp>Speaker 1: It's great. Let's fetch the actual data. Right? So we're going to read the let's just call it get registration. Find registration, whatever you want to call it.\u003C/p>\u003Cp>From the trigger, we're gonna use full access and collection here. It's gonna be registrations. And basically, what we're going to do here, I'm going to drill through the related fields. So we're gonna do this. We're gonna get all the root level fields of the registration.\u003C/p>\u003Cp>I don't\u003C/p>\u003Cp>Speaker 0: really need to do that, but,\u003C/p>\u003Cp>Speaker 1: we've got the event. I'm gonna do dot dollar symbol or dot asterisk. I'm sorry. And then the user, we're gonna get the user details. User dot k.\u003C/p>\u003Cp>Savvy. Am I forgetting? Oh, Should only be using quotation marks. Alright. Okay.\u003C/p>\u003Cp>So that should give us the fields for the registration and should give us our related data. And now we're gonna actually send this email out. So we'll send the email. Oh, actually, let's back up. We'll save that.\u003C/p>\u003Cp>I do need the key for this. So I need to figure out what IDs we have. That's gonna be dollar sign trigger dot key. I think it's just gonna be trigger dot key. We're using that dollar sign as a special operator just to access the actual trigger data.\u003C/p>\u003Cp>That's the only operation though that we need to append or prepend that dollar sign to to actually access that data. Alright, so we've got the get registration. So this is what our email is gonna be. It's gonna be get underscore registration dot user.email. That's what we'll save that to.\u003C/p>\u003Cp>You are registered for git_registration.event.title. I think that's what it is. And what are we gonna do? Here's the URL to access the event. Alright.\u003C/p>\u003Cp>And then we'll say link, and then we're gonna add the link for this. And for our link, we're gonna have what? HTTP local host 3000/events /getregistration.event.slug. Okay. Alright.\u003C/p>\u003Cp>So we've got this. Let's hit save. We save. And now we cross our fingers. Right?\u003C/p>\u003Cp>I'm gonna go back. We probably need to get our registrations for each user, but I'm just gonna go in and register. We'll see if this flow actually ran. So I can see the flow here. Did we get anything wrong?\u003C/p>\u003Cp>That looks really nice. Let's check my email address. Boom, there's our email. And if I click the link, boom, there's our page. Right?\u003C/p>\u003Cp>Amazing. Alright, so, what do we want to do? We probably need to check registration, but, you know, what happens if they're registered, the time is starting, we want to show this specific event. Alright. So one of the tools that I've used in the past, is called Whereby.\u003C/p>\u003Cp>They have this nice embedded functionality where you can create a video call or a webinar experience on your own site. So that's huge, especially for folks like me that that want to build their own tools that are very design oriented. I refer to myself as a recovering designer. We're gonna go in. I could create a room with no code, but the more interesting bit here is creating this via the API.\u003C/p>\u003Cp>So let's take a look at how we do this really quickly. We want to embed Whereby into our app. They have a web component for this, but we have to generate the the room first. Right? So here is create a transient room.\u003C/p>\u003Cp>I don't know what a transient room means, but, bedding I I guess that's what we want is a transient room. Create some transient room that is available between creation and an hour after the given end date. Alright. So here's the post request, here's the structure of this. We probably need to get an API key to do this as well.\u003C/p>\u003Cp>So let's go into my Whereby application. We'll go to developer guide. No. Go to settings. Members, profiles.\u003C/p>\u003Cp>Where where is my API key? Configure. Generate an API key. Okay. Event platform.\u003C/p>\u003Cp>Okay. There's our API key. We close. We've got that. Alright.\u003C/p>\u003Cp>So let's change up our data model just a little bit where we're gonna add the event. So for our event, we've got a Whereby Room. Actually, let's go back in and we'll just set this up. Let's add our rooms to this. Right?\u003C/p>\u003Cp>Room. Alright. Take created. Just add all of this. Fancy, fancy.\u003C/p>\u003Cp>Alright. Each room is gonna be attached to an event, so we'll have a mini to 1 on the events collection or to the events collection. And if I go into advanced field creation mode, I'm going to go in and add the reverse. Right? So on the events collection, we're adding a rooms field that will have an array of the rooms.\u003C/p>\u003Cp>Cool. Alright. So we got events, and then we're just gonna have, what, a whereby room URL. Alright. Let's see if we can get this done in time.\u003C/p>\u003Cp>We are crunching for time. Alright. Okay. So now we'll say anytime we create a room, let's create a Whereby Room. Create Whereby Room.\u003C/p>\u003Cp>So anytime we create a new room within an event, we're gonna trigger a webhook request. So Action Non blocking, Items Create, Rooms, and we're going to call the API for Whereby. Whereby. We're going to I think it's a post request. Right?\u003C/p>\u003Cp>The header, we're gonna be authorization. Bear. Might include that token that I had. Alright. So where'd we go?\u003C/p>\u003Cp>Create meeting. API\u003C/p>\u003Cp>Speaker 0: meetings,\u003C/p>\u003Cp>Speaker 1: post request, Content type, I'll add that header. Content type, application, JSON. Amazing. We'll go in. The end date is what we need.\u003C/p>\u003Cp>End date is the only thing that is actually required. So we'll do end date. Just copy what they have here. I think I missed 1. 202411.\u003C/p>\u003Cp>I'm just gonna hard code this for now. We'll say 425. Push it a couple weeks out. So the room mode is probably something we're gonna set as well, just because we want it to be a large room. Room mode.\u003C/p>\u003Cp>Group. Okay. And then we've got our request body. That should give us what we want. And then the response is gonna be a room URL.\u003C/p>\u003Cp>Alright. So then we're going to update. Oh, I had it right. We'll update update room. K.\u003C/p>\u003Cp>We got rooms. We got from the let's do full access, and we'll say, what's it gonna be? Trigger dot payload trigger dot key, actually. Should be. Alright.\u003C/p>\u003Cp>And then the payload what did I call this? Whereby room URL. K. And then we'll populate that data, whereby dot data. What's gonna come back from this?\u003C/p>\u003Cp>It'll say room URL. Cool. Let's run it and see. See what we got. So we'll go to this event, Leap Week 2000.\u003C/p>\u003Cp>We'll create a new room. Maybe we need a title for this room as well. Let's add that. Title. Alright.\u003C/p>\u003Cp>So we add a new room under Leap Week 2000. Let's add a new room. This is a test room for Whereby. And we had saved we saved this. Did it update?\u003C/p>\u003Cp>No. It didn't. There's something going on. Create whereby room. What did we get?\u003C/p>\u003Cp>Bad request. Why did we get a bad request? Whereby body, end date room mode. Is there something else that we're missing? Content body dot JSON dot stringify.\u003C/p>\u003Cp>Room mode\u003C/p>\u003Cp>Speaker 0: equals group. Room URL.\u003C/p>\u003Cp>Speaker 1: Whereby embedded? Where is this? Configurable room pages. Directs plus. Where's this developer guide?\u003C/p>\u003Cp>Embedding whereby in an application. The web component is included in our SDK. I'm gonna man, we're just gonna cheat. It's not cheating. Right?\u003C/p>\u003Cp>We'll just create a room. This is gonna be a large room. No cloud recording. We're not gonna record this. We're not gonna do any automatic streaming.\u003C/p>\u003Cp>We'll set this up for the end of April. Room prefix, Leap Week 2,000. Create the room. Alright. There's a participant link for the room.\u003C/p>\u003Cp>Yeah. We could go in and customize this room, but, hey, we're running short on time. Alright. So now we go back. How do we where's the embed?\u003C/p>\u003Cp>Alright. We got this web component. We are going to install. Alright. So we go to node, pmpm@wherebybrowser.\u003C/p>\u003Cp>Ppmi@wherebybrowser. We're cutting this extremely extremely close. We'll wait for that to install. Okay. PMPM dev.\u003C/p>\u003Cp>Alright. So we're gonna go back to our events. We'll do rooms, slug. No. This will be room ID.\u003C/p>\u003Cp>That's what we'll roll with. Room dot ID. Just gonna paste basically all of this. Right? Alright.\u003C/p>\u003Cp>We'll sort this out. We're gonna request the room. We're gonna actually read an item because we're gonna have the ID. Read item. And then it should be the route dot params.id.\u003C/p>\u003Cp>I think that's gonna give us what we want. We don't need to transform that. The room data, And then we're gonna need to import this as well. So we got this imported. Read room.\u003C/p>\u003Cp>Direct to site request. Alright. Let's see if we can actually get this working in time. Proom.title. Nothing fancy.\u003C/p>\u003Cp>And then we have this whereby embed, and we can make this pull in. We've got room dot whereby room URL. Is this actually gonna get us done? And then within the event, we actually need to list those rooms. So class l\u003C/p>\u003Cp>Speaker 0: I v four\u003C/p>\u003Cp>Speaker 1: i.roominevent.roomskey equals room dot ID. Okay. Okay. Hit me. Where we at time wise?\u003C/p>\u003Cp>We're we got, like, 3 minutes left. Let's see if we can actually get a list of the rooms. Where's the rooms? Where are the rooms? We did not set this up.\u003C/p>\u003Cp>We need to view all the rooms. Do we actually see the room? Where did I put that room? Where is it? What event is it under?\u003C/p>\u003Cp>Leap week 2,000? It's under leap week 2,000. Why are we not seeing it? Why are we not seeing you, mister rooms? Slug.\u003C/p>\u003Cp>Event. We don't see the rooms. Okay. Alright then. Again, we'll\u003C/p>\u003Cp>Speaker 0: just improvise. Rooms,\u003C/p>\u003Cp>Speaker 1: rooms slash. Not the right route. Events slash rooms. Document is not defined, it's, client only, isn't it? Whereby room?\u003C/p>\u003Cp>Maybe we wrap this in client only. Is that gonna fix it? Oh, no. What is it? Can you do I think you could do client only pages.\u003C/p>\u003Cp>Let's see what we got. Page not found. Pnpm dev rooms dot ID. Come on, man. We can do this.\u003C/p>\u003Cp>We can do this. Are we gonna get it? I don't think we're gonna get it. Page not found. Why are we not finding the room?\u003C/p>\u003Cp>What am I doing wrong? Rooms. Duh. There it is. Is that right?\u003C/p>\u003Cp>No. Directus dot request dot readitem rooms, route dot params dot ID. Why are we not getting the rooms? What is our request coming back from? Direct to snow match found.\u003C/p>\u003Cp>Events rooms. We are running out of time. We don't have time for this. Document is not defined. Okay.\u003C/p>\u003Cp>So that was working correctly. We're just not finding the room. Return request. Read items rooms.route.params.id. Should be events slash rooms dotid.\u003C/p>\u003Cp>Boom. We ran out of time. Not sure exactly where things have went wrong here. Things happen sometimes. The page\u003C/p>\u003Cp>Speaker 0: not found. We are not\u003C/p>\u003Cp>Speaker 1: finding it. Don't know where we went wrong. Route.params.id should be there. It is not. Sometimes you have to admit defeat.\u003C/p>\u003Cp>So unfortunately, we have not completed the things that we needed to. Let's take a run at this and and just give a recap. Right? So what did we do? We had set up an event, allow registrations, we had a public landing page, we had room support, but we did not get to the actual event platform.\u003C/p>\u003Cp>So this is a pretty neat experience, though. If you wanna check out Whereby, they're really cool for building these things. You can actually, go to this room. Now you can see that I can request permissions from this. And, you know, you can actually serve this inside your application.\u003C/p>\u003Cp>Yeah. Super helpful thing. So that is this episode of 100 Apps, 100 Hours. I hope you enjoyed it. What a fun ride.\u003C/p>\u003Cp>We'll catch you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps. 100 hours where we try to build your suggestions or some of your favorite apps in 1 hour or less using Directus or publicly fail, die on the vine trying to build something. If you're new to the show, there are only 2 rules. The first rule is we have 60 minutes to plan and build, no more, no less. The second rule is the anti rule. Use whatever you have at your disposal. So then the brakes. Let's dive into today's episode. Right? We're gonna be building an events platform, a virtual events platform. Now that sounds to me very very vague, could mean a lot of things to a lot of different folks. But, what kind of products are we talking about? So Zoom comes to mind as far as, like, registering a meeting and performing that meeting. You know, maybe you've got some interactivity like a chat, some registration. You know, you've seen this beautiful, beautiful, beautiful registration page before, I'm I'm sure. Not knocking you Zoom, but, like, with all the engineers that you guys have, why is this the best that we get for a registration page? I don't like it. Other softwares that I've looked at, like, DemoHop is an interesting one that, you know, has, like, a science fair where each there's a bunch of different booths, some of the other ones, like, Livestorm. There's a ton of these different webinar platforms every way that you slice it. But what I've discovered and and just in my own experience with these over the years, it's nice to have your own. So that's what we're gonna be building today. We're gonna be building our own events platform. Let's run through it. We'll start the clock, pull up 60 minutes, and away we go. Okay. So, I am in FigJam here. Let's just kind of mock out our functionality that we like or what we're looking to see out of this particular app. So, as far as our functionality, this is a nice serif font. Maybe we underline that as well. Great. Alright. So what do we wanna do? We wanted to set up we can't have all of it underlined. That's not cool. Set up an event. You know, allow registrations. You know, have a public landing page, registration page. What else do we want? We wanna have chat during the event. Maybe we wanna have different rooms for the event. You know? You think a virtual event, it could just be just, hey. It's a workshop. It's a webinar. Could also be like a week long leap week like we do here at Directus. So maybe we have rooms for that. This this seems like I'm getting way in over my head, so we'll stop there. And then let's let's kinda just flesh out our data model for this a bit. So in my mind, what are we gonna have? We're gonna have events. K. And we got that nice purple color. Maybe we'll make this a little bit bigger so we could see it. So we've got events and then we're gonna have, users or attendees. You know, that's gonna be our Directus underscore users collection. We'll just pick that up. Directus gives us that out of the box. No reason not to use it. Amazing. And then I would imagine there's a junction table here, right, where we have could have multiple events. So these will be, like, registrations, maybe. Naming stuff is always the hardest part of development. So we got registrations, draw some arrows, just connect these, connect these in your mind as well. Great. And then what else? You know, if we jive with the rooms, you know, we could have rooms as well. Right? Each room has a different topic, some schedule, etcetera. If we think of, like, our events, they're gonna have a date, start date, duration, or maybe an end date, title, description. What else do we have? Registrations, obviously. Probably an image. Schedule, potentially. Cool. Alright. Getting too carried away. Anyway, let's actually start building something. Right? This feels just roughly what we're looking for. So let's dive into it. Right? What have I got set up for this? Just gonna close all these other things. I have a Directus instance. I've got a Nuxt 3 application, just a starter application set up. It's got a login and register function. I don't even think the register actually works on this thing, but that's what we're working with. Let's go ahead and get logged in to our Directus instance. What did I set the password to? I think it's just password. Real secure. Alright. So we've got a blank instance. I've got my FigJam over here on the left. Got Directus on the right, a 125% zoomed in, and let's just dive in. Right? Our first collection, we're going to start here with our events and we'll create a new collection for that. Let's give it a UUID, When it was created, when it was updated, those are all great. Let's do status as well for like published events, draft events, completed events. Let's just take a look at that. Right? So we have published, we have draft, we have archived. I could go in and flesh these out further if I wanted to or we could just leave it at that. Maybe draft is the default. That's good. Solid. Alright. So here, we're gonna have a name for the event or a title. The event title, that's just gonna be a string. A description of this, we want to be the anti zoom here, so we're gonna make this a text field using the WYSIWYG interface for Directus. We're going to call it description. That's our key. And I could adjust our toolbar if I wanted to. Maybe I want to add undo and redo. I'm not a big fan of underline, so I'll just remove that. If I need to, I can dive into advanced field creation mode, and specifically with the WYSIWYG editor, you can you've got some formatting options here and options overrides that you can pass to the tiny MCE instance if you need to. Like, you know, maybe you have a special class for buttons or something like that that you want to display inside that WYSIWYG editor. Alright. Let's add an image for this. So just a single image. You know, we could potentially have, you know, multiple images, but we'll just keep it simple. That's what we're going to show on a card. As I'm imagining this, on the front end, it's It's just like a running list of events or maybe a grid of cards for the events. When you get inside the event, you've got to register. Then once you register, maybe you can see the schedule for the event. Cool. So as far as something like a schedule, right, how should we set this up? If I'm not really sure, like, one of the things that I do a lot, I'm not really sure what this is gonna look like immediately. Instead of creating another table, I may use something like the repeater, which is basically a JSON type field. That's what it's going to save as our, inside our SQL database. But let's just call it schedule, and then I could go within this and edit fields to repeat, like a set of fields. So, I don't know. I like a speaker. It'll probably be something else. Let's just say the name or label. Okay. We'll do string. That requires a value. What's the name of this thing? And the interface would be an input and then we could do something like time, date, time, time. Start time, Start time. Sounds great. That's what we'll roll with. Let's make that full width and then we'll use the timestamp format. We'll require value. Maybe we don't require value. May not need to pick a time. Maybe we're just showing scheduling order. When does this thing start? Cool. Alright. Sweet. So now we have some events or at least the the start of something for events. Let's go in and go back to the data model settings. We're just gonna do registrations and we'll do, like, a date created, user created. Do we even I don't even think we need this. Status. Do we have a status for the registration? Maybe it's canceled or something like that. I'm reading too much into it. Right? So here's the date created. Maybe I wanna show this, so I'll click the drop down, show that field. Maybe we make it full width, just in case. And for our registrations, we're gonna start digging into our relationships. Right? So when I think of a registration, we've got the event that that registration belongs to, and we've got the user that made the registration. So those are both gonna be basically many to one relationships. So we'll go in for the key, we'll add the event, we'll just do events. And for our display template, let's use the title. I can already tell that I forgot to input my start and end date for the event. Amazing. So we'll have to go back and fix that as well, but there's our event. Let's go ahead and create our relationship to the users. So we'll say the user that's registered or, you know, this could be like the attendee. Again, naming stuff is hard. Cut me some slack. Alright. So I want to pick the directus underscore users collection. I'm just going to dig into the systems drop down here, or system, choose direct us users. I can get fancy with the display on this. Give them the avatar with a nice little thumbnail. We'll do first name, last name, and behind the scenes, this is just mustache syntax. So I can edit these raw values if I want, do it that way, or I could just pick from the drop down. That's great. We don't wanna create new users from this. We could potentially select users. Great. Okay. So that's our basic data model. Why do we have event? Some type of bug. Alright. Regardless, we have events, we have registrations. Let's go in and add our start time for this. How could I forget? So in this case, we wanna save the time zone value, so we're gonna use time stamp. This could be the start date. Maybe we have an end date for this or, you know, potentially a duration. I guess you could do either or or both. You know, you could potentially even use flows to calculate that if you wanted to. But this is good. Let's add some icons just to fancy it up a little bit. My friend Kevin on the team here will be sweating if you watch me add these icons, but, there's always room for design. Cool. Alright. So we got events. Let's create our first event. Right? What are we gonna call this? Let's call it Leap Week number 2 1000. Leap Week 2 1000. We are taking a huge leap forward for Directus. Join us as we take a huge leap forward for Directus. Lots of cool stuff. Let's add some formatting to this as well. You know, thing 1, thing 2, thing 3. Cool. Alright. And then let's add an image for this. Right? So let's just search Directus Leap Week and see if we can grab, do we have any images? Yeah, we got a couple laying around. Cool. So we'll save this image. Is that right? Hope that's the right image. We'll just upload that. Everything announced at our 1st Leap Week. And let's set a start date for this. Let's say May 15th is a good date. Maybe this runs 2, 3 days. Yeah. Great. So for our schedule, we've got workshop with Bryant. That is the I forget what day we did this now. 17th. Oh, that's it. May 17th. Not April 17th, Brent. Alright. Let's say that is at 3. Great. Cool. So I'm gonna fill out the schedule workshop with Kevin. Go for 15th at, at 16th at 1 PM. Great. Alright. So now we've got our event. Maybe let's just drag this out a little bit, move this around. Okay. One of the other things that I forgot here is probably like a slug. Right? I want a nice URL on the front end. So we'll go back into events, we'll just create a new field for it, we're going to use the input, call it slug. The other thing that I'm going to do is go in and make this URL safe, so we could slugify this thing. Now one of the other cool things with the addition of the marketplace, let's look for a slug, Permalink interface to enter URL slugs. Is this what we're looking for? No. WP slug interface. Yeah. This is cool. Alright. Let's try this out. We'll hit refresh, install this extension. I didn't have this on the first season of 100 apps, 100 hours, But now I can go into our interface and we can choose the slug interface. And our template is going to come from the title and we'll auto generate on create. Let's try that and see what we've got. We shrink the title and the slug. Cool. Alright. So now we got a couple settings. We can hit auto generate. Generates this nice slug, and it looks a lot like the old days of WordPress. I mean WordPress is still around, obviously, but this is this is nice, I like it. Alright. So we've got events, we've got registrations. What do we do next, right? We're gonna look at access control. So this is our settings of who has access to what inside Directus. There's 2 default roles, public and administrator. Let's make the public roles, we want to be able to view the events, We don't want anybody to edit events. And then, for as far as registrations, maybe we want to be able to create registrations and then maybe just read, you know what? Let's give full access to that. And, that should be maybe what we want, enough to play around with. Right? So now what are we going to do? Right? Let's dive into the front end of this application. Alright. So I'm going to pull this up. Again, I've just got a a Nuxt application here, and let's flesh out a couple of routes. Index dot view. We can make this just a list of our events. Alright. So we're gonna go in. I've got Directus configured as a Nuxt plugin here. It's just got some authentication in it. We've got real time in case we ever needed it. And then we've just got the REST client. That's the way I prefer it. And we just provide that to the Nuxt application. So we'll call that up like this, const directives equals use Nuxt app, and in this case, we're probably gonna do, like, read items as well. So we'll grab that from import items from the atdirectus SDK. And, alright, what's next? Right. So we're gonna do some data. We'll use await, use async data, and we'll do return directus dot request read items. And we're reading from the, what, events collection. And then I have a, query for different parameters. Right? We wanna show maybe we only wanna show published events, right, so I could do this. We'll just say status equals published. Thank you, GitHub Copilot. Let's see what that gets us. Directors. Cool. Maybe we scrap all of this. Would do we even need this to be flexed? Probably not. Alright. Let's see what we got. Data. Just wrap this in like a pre tag. See if we get any actual data. Right. So we've set up this filter. We're not getting any data because I'm assuming we did not publish this event. So if I go in and publish this, let's see what we have now. Okay. Cool. So we have our event information. What can we do with this, right? So I do also have the, like, Nuxt UI library built into this. So if you just go to ui.nuxt, using that, it's got, you know, some components already built in for buttons and cards and things like that. Cool. So without getting too fancy on the UI, let's just mock something up now that we can see we've got some actual data. Right? We got a diff. We add some padding. Y 12p y 6. We add a, do we even need an h one? Let's call it h 2. This will be font bold. Text, let's make the text large. And let's make it mono, font mono. Alright. So will be events. See what we got there. There's our events. And now let's do a grid. Right? So we'll just do a div to wrap this. Just grid, maybe grid calls to like a break point, maybe we set the gap to 8, and then we're gonna do a v 4. Right? So, for our events, let's just use this card. I think there's a card component in here. I feel like a nice card component is ucard. We've got, like, a header and a footer, or we could just throw it in the middle. So let's do that. Right. V 4. We're gonna do events and data. You know? And, usually, I like to destructure those. Yeah. Yeah. Maybe we call it events. And for the key here, we'll use event ID. That way, we don't have to, use, like, an index or something like that, and then we'll actually do this. Alright. So is there actual I think there's, like, a text component here, so let's just do, like, an h three. Maybe we just take the auto completion here from, GitHub Copilot. We'll do a little Nuxt image to optimize these images. Works very well with Directus. There's a provider for it. Then we've got the event dot image. We use the event title. That seems pretty good. Let's see what we come up with here. Sweet. Except for our description. Right? This kind of sucks because it is not what we want. So in this case, we'll do div. Let's do v h t m l event dot description, and remove that. Great. It has no formatting. Maybe we've got, do I have to tell when? We got pros class. Yeah. There we go. Alright. So we've got that, and let's add a link for this. Right? Maybe we add a little spacing here, n t 4. Maybe we make that add a little spacing here. MT2, MT8. It's great. Cool. So now we've got an event looking really nice. You know, I could duplicate this event if I wanted to. Let's just see what this looks like with a couple of more events. We're going to call this Leap Week 3000. Flush this out. And what I'm going to do here, we're just going to open this image up. I'm going to get really fancy on this, guys. And we'll just mark this up. Scroll this out. 3000. Oh, so fancy. Alright. For Leap Week 3,000, we're going to close this out, upload a new photo, we can see that's Leap Week 3,000. And then what I'm going to do here is click Save and Copy from the 3 dot menu button and now I've got 2 events. We've got Leap Week 2000, Leap Week 3000. There we go. Amazing. Okay. Alright. So now we need a route for our actual event. Right? So let's set this up. Let's actually add a folder for it just in case we have a couple of other routes within that. And then for our events, we're gonna have a slug dot view. So that's just gonna be a dynamic route. We'll view I think it's init. View init. What does that give me? That's not the syntax that I like. It'll do. Trying to make it fancy. I need to get my snippets set back up. I had to scrap Versus code. Lost all my snippets. Alright. So we got a slug. We're gonna use the route here. So we've got route equals use routes. Cool. And then, on the individual page, right, we want to call the specific data. And, you know, maybe we don't even need this full description on our index page, so we could, you know, potentially leave that off. Maybe we just make it the event start date, but we may wanna use that in here. And let's use just, like, a date function. I think it's format from date f n's. Formats oh, not currency. I'm gonna do format. What we got? DDD, month month, DD. Where does that get us? Doesn't get you anywhere because it breaks it. Forgot the year year. I guess it's all lowercase. And then okay. Yeah. Still still got a ways to go memory wise. So date f and s, just a handy library. We got format. What's the date? What are we looking for? Like, month is gonna be m m m. Like that one. So we'll do m m m. And what if we do, like, the ordinal of the dates, day of the month, d o? That's what we want. Now we're looking nice. Okay. And then we wanna do the time. Right? So maybe we show the time. Local day of the week, AM PM. What's this gonna be? H. What minute? M m m. And then we've got AM, PM, AAA. Wasting too much time on this. Okay. Alright. So we got that. Let's go in and just wrap this card in a NuxLink, or at least the content of the card. We'll do 2. And let's make this dynamic. That'll be /events/ event.slug. Uh-oh. Oh, forgot to close that. Cool. Alright. So now we've got the links set up, so we can click on these and and move to the route. We don't really have anything inside our route though. Right? So I could take this function here, just copy that in, and here we're still going to be reading items. Right? I'm going to call this event, in addition to status. I guess we can set this up where if you call it directly, even if it's a draft, maybe you wanna see it. It. So let's just do this. We'll say slug equals route dot params dot slug. That should get us the event that we want. And then, the actual event here, we're gonna do, like, a transform function. The nice little part that's baked into the use async data call, that composable inside Nuxt, where basically this is gonna return an array. I could transform this and just pick up the first item of this array. And now if we do this, just do pre class. Again, I always like to do this just to see what data I'm getting back. And now we've got just the specific data object for this item. Alright. What's next? Right? We're gonna dive into building this actual page. Right? So let's give it a nice h one tag. We've got font bold, font mono, text let's make this giant. We're gonna do the event dot title. Okay. Leap week 3,000. What else do we have? If I dig into the clipboard, I've got that event description. We can make this pros large. We wrap that. Let's move down. We'll do another div. Throw this into an image, event dot image, class w 96. Where's our actual image at? Okay. There's our image. We could flex these 2, add a little bit of padding. I mean, this is not gonna win any design awards, but, hey, it'll be alright. We'll just take these items from the first one. Let's put this below. Do we have our dates? Oh, gotta actually import that function, don't we? Import format from datefns functions, I guess. Alright. So we got our date. Add a little margin to the top. Looking great. And then what if we wanted to show that schedule? Right? So, maybe we drop down over here on the left. We have a list. And within the list, take a little GitHub completion. That's not really what we're looking for. It's gonna be the item inside the event dot schedule. For the key, we used you the item dot label. And then we have yeah. I was wasting more time using the GitHub suggestions. Item dot label. Item. We'll just steal this format, and can we do this as well? Dash hmmaa. What does that give us? Does that get us where we want? Okay. So now we can see the schedule. We can drop a little schedule class. List. List inside list disk. I guess, is that the list item? Why are we why does that look weird? Not sure. Not gonna bother with it. Let's do font bold, font mono, empty 8. Okay. Cool. What else we got? So we got the schedule right. Good. We got a page. We could go back to our events. Yeah. Maybe we add a link for that up here somewhere in the top. Next link to equals slash slash I think it's just slash. Back to main list of events. Alright. Cool. We got all this going on, we could go into Leap Week 3000, we could go to Leap Week 2000. How are we doing on time? We are cruising on time. Alright, so how do we set up registration? Let's go in and what we're going to do, we're going to need to basically authenticate the user first. So we've got to have a user to register. And I am going to cheat here and use, like, a SSO, like, register with GitHub. I've used this in a past project. So I've just got some code over here. We're gonna go in and inside my env or my Docker Compose file, I would set this up. I'm missing the client secret and client ID from GitHub, so go inside my GitHub account. We're gonna go to developers, we'll go to applications, is it authorize no. Developer settings, OAuth apps, we're going to create a new OAuth app. We'll call it Events Platform. Super secret. The homepage URL will be HTTPS. We'll just use Directus. Do we need we need this callback. And just from past experience, the structure for this is gonna be my directus instance slash auth /login/provider. So we're using GitHub, that's gonna be the provider. We're gonna use that callback. Great. And then I can get my client ID. I can get my client secret. We'll just have to use a password, log in, get the secret, they'll steal my secrets, bro. Okay. And then the last piece of the puzzle here is we need a default role for that, for that specific user. So what we're gonna do, we'll go into Directus. Let's create a new role for attendee. And we're not going to give them app access, we're just going to save it. I'm going to pick up the actual ID for this role, I can see it over here. That's the primary key. We'll paste that in there. I'm going to save that, and then I'm going to stop this container, Docker Compose up one more time, and the effect I should get if I load this again, now I can see this login with GitHub function. So I could actually log in with GitHub, and let's see. Oh, yeah. I'd have to actually do my username. Maybe we swap this out. Alright. We get a local host, 8055. Log in. Admin, example, password. Alright. And then I log out here. We could test this out. But I could also do this on the front end as well. Right? So let's go back to our page. Alright. So I'm gonna stick a big registration button over here somewhere, and I think I've got one in register. Is it the register form or the login form? Do I have it? Yeah. There we go. I got this button that I've used previously. We're gonna go to our event page. We'll save this. Uncomment this button. I don't really need the divider label. Alright. Signing with GitHub to register. Cool. And here you can see I've got a redirect, just basically to a different page. We're going to make this size excel, giant button, just add a class to it, give it a little more padding. Duplicate Attribute. Oh, MT 12. We don't want to duplicate that class. Alright. So what is this going to do? This should ask me to log in once accessed to my GitHub account. We authorize, and now I've got, like, the index page. So if I go here, if I actually if I signed in, right, probably getting some errors here because we did not give permissions to those specific accounts or that specific role. Right? So for our attendee role, we want them to view all the events, we want them to create registrations, maybe they can view the registrations where the user equals current underscore user. Alright. We're gonna have to run like hell here. Apologies for the curse words. Right? So okay. If you I look at my Nuxt state. I've got a use state composable that's actually storing our user data. So I should be able to see that user data. I've got the ID of the user. Do I have any other information about it? Maybe that's really all I need. Right? So let's try this. We'll do const user equals use state, user. Will that give us access to the user? And if we have a user, we can hide this button And maybe if if no user. Okay. And then maybe we add another button. Register. And on this one, we're gonna add a click handler. It's gonna be register for event. Okay. How's that looking? V f user. Cool. And now we need a function for that. Right? This is gonna be an async function because we're gonna call directus. We are going to import, we need the create item. Okay. Async function, register for events. We're gonna call that. K. We'll do a try. Await direct us request registrations event dot ID. That's gonna be an actual reactive value, so we'll just add on that. We can add that around the user too just to be safe. And cool. What else did we do? Response. You know, I could just console log this response just to verify. Alright. So let's take a look. If I clear out my network request, we hit register, We've got registrations. Cool. Great. So now we're registered for this event. If I open my Directus Instance back up where are you, mister Directus Instance? We have registered for this event. I could see this specific user here. Maybe I wanted to send an email for this. Let's see how we're doing on time. Roughly 20 minutes remaining. Oh, we're cruising. So let's send an email event for the registrations. Send registration email. Registration email. Great. We'll go in. Event hook, so anytime an event happens, in this case we want the action, so after this action happens, we're not gonna block the thread. Anytime the new registration It's great. Let's fetch the actual data. Right? So we're going to read the let's just call it get registration. Find registration, whatever you want to call it. From the trigger, we're gonna use full access and collection here. It's gonna be registrations. And basically, what we're going to do here, I'm going to drill through the related fields. So we're gonna do this. We're gonna get all the root level fields of the registration. I don't really need to do that, but, we've got the event. I'm gonna do dot dollar symbol or dot asterisk. I'm sorry. And then the user, we're gonna get the user details. User dot k. Savvy. Am I forgetting? Oh, Should only be using quotation marks. Alright. Okay. So that should give us the fields for the registration and should give us our related data. And now we're gonna actually send this email out. So we'll send the email. Oh, actually, let's back up. We'll save that. I do need the key for this. So I need to figure out what IDs we have. That's gonna be dollar sign trigger dot key. I think it's just gonna be trigger dot key. We're using that dollar sign as a special operator just to access the actual trigger data. That's the only operation though that we need to append or prepend that dollar sign to to actually access that data. Alright, so we've got the get registration. So this is what our email is gonna be. It's gonna be get underscore registration dot user.email. That's what we'll save that to. You are registered for git_registration.event.title. I think that's what it is. And what are we gonna do? Here's the URL to access the event. Alright. And then we'll say link, and then we're gonna add the link for this. And for our link, we're gonna have what? HTTP local host 3000/events /getregistration.event.slug. Okay. Alright. So we've got this. Let's hit save. We save. And now we cross our fingers. Right? I'm gonna go back. We probably need to get our registrations for each user, but I'm just gonna go in and register. We'll see if this flow actually ran. So I can see the flow here. Did we get anything wrong? That looks really nice. Let's check my email address. Boom, there's our email. And if I click the link, boom, there's our page. Right? Amazing. Alright, so, what do we want to do? We probably need to check registration, but, you know, what happens if they're registered, the time is starting, we want to show this specific event. Alright. So one of the tools that I've used in the past, is called Whereby. They have this nice embedded functionality where you can create a video call or a webinar experience on your own site. So that's huge, especially for folks like me that that want to build their own tools that are very design oriented. I refer to myself as a recovering designer. We're gonna go in. I could create a room with no code, but the more interesting bit here is creating this via the API. So let's take a look at how we do this really quickly. We want to embed Whereby into our app. They have a web component for this, but we have to generate the the room first. Right? So here is create a transient room. I don't know what a transient room means, but, bedding I I guess that's what we want is a transient room. Create some transient room that is available between creation and an hour after the given end date. Alright. So here's the post request, here's the structure of this. We probably need to get an API key to do this as well. So let's go into my Whereby application. We'll go to developer guide. No. Go to settings. Members, profiles. Where where is my API key? Configure. Generate an API key. Okay. Event platform. Okay. There's our API key. We close. We've got that. Alright. So let's change up our data model just a little bit where we're gonna add the event. So for our event, we've got a Whereby Room. Actually, let's go back in and we'll just set this up. Let's add our rooms to this. Right? Room. Alright. Take created. Just add all of this. Fancy, fancy. Alright. Each room is gonna be attached to an event, so we'll have a mini to 1 on the events collection or to the events collection. And if I go into advanced field creation mode, I'm going to go in and add the reverse. Right? So on the events collection, we're adding a rooms field that will have an array of the rooms. Cool. Alright. So we got events, and then we're just gonna have, what, a whereby room URL. Alright. Let's see if we can get this done in time. We are crunching for time. Alright. Okay. So now we'll say anytime we create a room, let's create a Whereby Room. Create Whereby Room. So anytime we create a new room within an event, we're gonna trigger a webhook request. So Action Non blocking, Items Create, Rooms, and we're going to call the API for Whereby. Whereby. We're going to I think it's a post request. Right? The header, we're gonna be authorization. Bear. Might include that token that I had. Alright. So where'd we go? Create meeting. API meetings, post request, Content type, I'll add that header. Content type, application, JSON. Amazing. We'll go in. The end date is what we need. End date is the only thing that is actually required. So we'll do end date. Just copy what they have here. I think I missed 1. 202411. I'm just gonna hard code this for now. We'll say 425. Push it a couple weeks out. So the room mode is probably something we're gonna set as well, just because we want it to be a large room. Room mode. Group. Okay. And then we've got our request body. That should give us what we want. And then the response is gonna be a room URL. Alright. So then we're going to update. Oh, I had it right. We'll update update room. K. We got rooms. We got from the let's do full access, and we'll say, what's it gonna be? Trigger dot payload trigger dot key, actually. Should be. Alright. And then the payload what did I call this? Whereby room URL. K. And then we'll populate that data, whereby dot data. What's gonna come back from this? It'll say room URL. Cool. Let's run it and see. See what we got. So we'll go to this event, Leap Week 2000. We'll create a new room. Maybe we need a title for this room as well. Let's add that. Title. Alright. So we add a new room under Leap Week 2000. Let's add a new room. This is a test room for Whereby. And we had saved we saved this. Did it update? No. It didn't. There's something going on. Create whereby room. What did we get? Bad request. Why did we get a bad request? Whereby body, end date room mode. Is there something else that we're missing? Content body dot JSON dot stringify. Room mode equals group. Room URL. Whereby embedded? Where is this? Configurable room pages. Directs plus. Where's this developer guide? Embedding whereby in an application. The web component is included in our SDK. I'm gonna man, we're just gonna cheat. It's not cheating. Right? We'll just create a room. This is gonna be a large room. No cloud recording. We're not gonna record this. We're not gonna do any automatic streaming. We'll set this up for the end of April. Room prefix, Leap Week 2,000. Create the room. Alright. There's a participant link for the room. Yeah. We could go in and customize this room, but, hey, we're running short on time. Alright. So now we go back. How do we where's the embed? Alright. We got this web component. We are going to install. Alright. So we go to node, pmpm@wherebybrowser. Ppmi@wherebybrowser. We're cutting this extremely extremely close. We'll wait for that to install. Okay. PMPM dev. Alright. So we're gonna go back to our events. We'll do rooms, slug. No. This will be room ID. That's what we'll roll with. Room dot ID. Just gonna paste basically all of this. Right? Alright. We'll sort this out. We're gonna request the room. We're gonna actually read an item because we're gonna have the ID. Read item. And then it should be the route dot params.id. I think that's gonna give us what we want. We don't need to transform that. The room data, And then we're gonna need to import this as well. So we got this imported. Read room. Direct to site request. Alright. Let's see if we can actually get this working in time. Proom.title. Nothing fancy. And then we have this whereby embed, and we can make this pull in. We've got room dot whereby room URL. Is this actually gonna get us done? And then within the event, we actually need to list those rooms. So class l I v four i.roominevent.roomskey equals room dot ID. Okay. Okay. Hit me. Where we at time wise? We're we got, like, 3 minutes left. Let's see if we can actually get a list of the rooms. Where's the rooms? Where are the rooms? We did not set this up. We need to view all the rooms. Do we actually see the room? Where did I put that room? Where is it? What event is it under? Leap week 2,000? It's under leap week 2,000. Why are we not seeing it? Why are we not seeing you, mister rooms? Slug. Event. We don't see the rooms. Okay. Alright then. Again, we'll just improvise. Rooms, rooms slash. Not the right route. Events slash rooms. Document is not defined, it's, client only, isn't it? Whereby room? Maybe we wrap this in client only. Is that gonna fix it? Oh, no. What is it? Can you do I think you could do client only pages. Let's see what we got. Page not found. Pnpm dev rooms dot ID. Come on, man. We can do this. We can do this. Are we gonna get it? I don't think we're gonna get it. Page not found. Why are we not finding the room? What am I doing wrong? Rooms. Duh. There it is. Is that right? No. Directus dot request dot readitem rooms, route dot params dot ID. Why are we not getting the rooms? What is our request coming back from? Direct to snow match found. Events rooms. We are running out of time. We don't have time for this. Document is not defined. Okay. So that was working correctly. We're just not finding the room. Return request. Read items rooms.route.params.id. Should be events slash rooms dotid. Boom. We ran out of time. Not sure exactly where things have went wrong here. Things happen sometimes. The page not found. We are not finding it. Don't know where we went wrong. Route.params.id should be there. It is not. Sometimes you have to admit defeat. So unfortunately, we have not completed the things that we needed to. Let's take a run at this and and just give a recap. Right? So what did we do? We had set up an event, allow registrations, we had a public landing page, we had room support, but we did not get to the actual event platform. So this is a pretty neat experience, though. If you wanna check out Whereby, they're really cool for building these things. You can actually, go to this room. Now you can see that I can request permissions from this. And, you know, you can actually serve this inside your application. Yeah. Super helpful thing. So that is this episode of 100 Apps, 100 Hours. I hope you enjoyed it. What a fun ride. We'll catch you on the next one.","8c0c2643-c249-4bf7-8319-87fb4758fe08",[440],"bd184dc4-368f-468a-815a-f2a8e8b246c0",[],{"id":157,"number":158,"show":122,"year":159,"episodes":443},[161,162,163,164,165,166,167,168,169,170],{"id":164,"slug":445,"vimeo_id":446,"description":447,"tile":448,"length":267,"resources":8,"people":8,"episode_number":135,"published":449,"title":450,"video_transcript_html":451,"video_transcript_text":452,"content":8,"seo":453,"status":130,"episode_people":454,"recommendations":456,"season":457},"realtime-leaderboard","936355804","Bryant races against the clock to build a real-time leaderboard for the Directus Arcade game – Duckin' Cold Emails. Watch as he builds features to submit and display high scores for the game using Directus Realtime and websockets.","b05a9814-7065-4816-841c-74e263ce5b20","2024-05-10","Mission: Realtime Leaderboard","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, and today we are building something that I think is pretty cool. But here on 100 Apps 100 Hours, we build your suggestions, your favorite apps, or rebuild a clone of some other app in 1 hour or less, or publicly fail, get humiliated, trying. It was a lot of stress, to be honest with you.\u003C/p>\u003Cp>So the rules for this, number 1, we have 60 minutes to plan and build an application, no more, no less. And number 2, just use whatever you have at your disposal. So, could be AI, could be past projects, whatever we've got. We're just trying to speed run through applications. And today we've got a cool one.\u003C/p>\u003Cp>We're going to be building a real time leaderboard. So recently, as far as, like April Fools' Day, we put out this amazing game to make you a duckin' pro at cold email. So it's called duckin' cold emails. It's just a runner game where you face off against a sales rep who is sending you cold emails and pop ups on the screen here. So you duck or jump, you get a couple of lives, and at the end of it you can choose to play again, and you get a nice little score.\u003C/p>\u003Cp>So what we're going to do here is create a leaderboard for this, and riggity jig, away we go. Alright. So, 60 minutes to plan and build. Hopefully we don't need all of that, but maybe we will. That's how this works.\u003C/p>\u003Cp>Alright. So I usually like to start by mocking out the functionality that we need out of this. So what do I really want to happen here? When I play, I wanna be able to submit my score and if somebody else is playing around the world, I want that score to and they get a high score, I want that to pop up. I wanna see that.\u003C/p>\u003Cp>So we've got a real time tracking of high scores. This is really large. Let's just trim that down. Real time tracking of high scores, submit a username and message with that, maybe? Maybe we'll make this a little more competitive and that we can talk trash to each other while we do it.\u003C/p>\u003Cp>What else is there really? Display those scores. Display high scores. I'm gonna put maybe sound effects. I'm gonna put that in question mark.\u003C/p>\u003Cp>Maybe that'll be our stretch goal. So that's that's basically the functionality of this. Right? I don't know explicitly how long that's gonna take, but what do we have as far as a data model? That's one of the other things that I like to map out.\u003C/p>\u003Cp>We've got some actual players, but, you know, I'm not going to have a user that needs to log in for this. I'm just going to let we're going to do it NBA Jam style, where you can just key in your initials or something like that. So I'm just thinking here, there's probably only maybe just a single table for this, which is rare for us on a 100 apps to a 100 hours. We're going to have a value for the score, a username, there's gonna be an ID and a time stamp of of when that high score was, and then maybe a message. That seems like it's it to me.\u003C/p>\u003Cp>I don't wanna make this over complicated. Alright. So I'm gonna go into my direct assistance. This is totally blank. We're gonna create a new table.\u003C/p>\u003Cp>We're gonna call it scores. Can I zoom in on this? Yeah. There we go. We'll make it really fancy.\u003C/p>\u003Cp>We'll do the generated UUID. We've got date created. So this is a system field or optional system field that will basically whenever this item gets created it will pre populate some stuff for me. So basically what it does on create of this record in this collection, it will save the current date and time. So we'll unhide this because I do want to see that, And then we'll add a value.\u003C/p>\u003Cp>So as far as the number of emails ducked, I can't duck half an email. So that's gonna be an integer. Looks great. What else do we have? Maybe we'll make that half width.\u003C/p>\u003Cp>And a username. So go sweet handle. Cool. And then we're gonna have a message. So we'll just use the text area for that.\u003C/p>\u003Cp>The type is text, of course, so I could add, like, markdown or something for that if I wanted to, but we'll just keep this short and sweet. Alright. So as far as our data model, is that that's probably it. Right? Looks good.\u003C/p>\u003Cp>Getting on my toes here for this one. Alright. So we got this, specific application. It's basically just a Nuxt single page application. If I look, I've got a single index page, there's an arcade cabinet component that is doing most well, it's not really doing hardly any of the work it's just presentation.\u003C/p>\u003Cp>It gives us this nice arcade cabinet and some text down below. And then we also have oh, these were my buttons I was trying the last time I was in here. And then we have a game component. So the library that we're using for the actual game here is pretty interesting itself. You might go ahead and check it out if you are trying to build a game.\u003C/p>\u003Cp>Did we start the time? Oh, yeah. We did start the timer. Okay. It's called Kaboom JS.\u003C/p>\u003Cp>A pretty interesting library, actually. Makes it really easy to build quick little mini games like this, build stuff that's fun for your users. Alright. So that's the meat and potatoes of it. You know, there's some sprites, there's some functions, there's a couple of different scenes inside the game, but that's what we've got, right?\u003C/p>\u003Cp>So we've got our scores, let's go about setting up our real time connection. So Directus has real time baked into the platform. If I pull up my other project that I'm actually connected to, you can see I have enabled my WebSockets here. And away we go. Alright, so as far as the Knox application, I've just got a simple Directus plug in that provides me with a real time client.\u003C/p>\u003Cp>That's really all we've got. I've set the auth mode to public just so I've got, you know, anybody can connect to this. Again, we're not going to make authentication required here. What I'm also going to do is the ability to read scores and push scores. Okay.\u003C/p>\u003Cp>So what else do we have? I think that's pretty much it. Right? Let's dive in. Let's go to our arcade cabinet.\u003C/p>\u003Cp>Pull this up. Right. This is our wrapper component. I'm not sure where I'm gonna stick the leaderboard. Maybe up here at the top, maybe somewhere down here at the bottom.\u003C/p>\u003Cp>We could just cannibalize this bottom section down here. I'll zoom out in a little bit. But we're gonna get started with the SDK and WebSockets. So we've got a great guide on the documentation for this. It walks you through this in great detail.\u003C/p>\u003Cp>But we're just gonna pull this up. I'm gonna pull that Directus client. Direct cuts. Directus. I can't spell.\u003C/p>\u003Cp>Alright. We're gonna use the Nuxt app. Use Nuxt app composable to get that client. And then what we're gonna do is await directus dot connect. Alright.\u003C/p>\u003Cp>So that will connect and if we keep scrolling down, we can see that we've got a, open function here. You know, if we just wanna log that this was opened. We do this and we go back. And if I look in the console, should be getting a message that this was opened. Switching protocols.\u003C/p>\u003Cp>Not seeing any messages in here for whatever reason. Yeah. I don't see any particular messages, but let's keep moving on. We probably do need this directus dot message handler. Directus dot on web socket dot message console log events.\u003C/p>\u003Cp>What if we just log the entire event? Actually, that'll be a message. Same thing. Let's see what we get here. Do we have that connection established?\u003C/p>\u003Cp>Where is our connection? Why are we not seeing any messages through here? Right? Okay. Let's just add a subscription as well and see what we got.\u003C/p>\u003Cp>Oh, could it be our permission settings? Right? Let's log in and see. Directory's password. Alright.\u003C/p>\u003Cp>So if we look at no. We've got access to that. Alright. We've solved that problem already. Alright.\u003C/p>\u003Cp>So let's just create a subscription and see if we get that. Alright. So we'll go in and do directus dot subscribe, and we're gonna subscribe to the scores, And we're gonna get, we're gonna pass some options to this. So we want the event to be create. The query do we actually need a query?\u003C/p>\u003Cp>I don't think we let's not limit ourselves at this moment. And then maybe I wanna add, like, a UID to this, and we're gonna call this the scores sub. Subscription direct us dot subscribe. And let's see what we got here. Okay.\u003C/p>\u003Cp>So now I can see that particular message coming through. There's our subscription for this. So we're actually getting some data. Let's take a look and see if we open this up in a separate window. So we'll just open scores up.\u003C/p>\u003Cp>We'll create a new scores item, see if we get subscription for that. Yep. Okay. I see some data coming through. Great.\u003C/p>\u003Cp>So we are getting some data for our scores, which is good. Not sure why it's not showing the open function. Not really concerned with that, anyway. Alright. So what we wanna do, just one moment, please.\u003C/p>\u003Cp>We'll stop the clock. Okay. Had a hot call come in. We're restarting the clock. No extra work has been done, and I've totally lost track of where we were at in the process anyway.\u003C/p>\u003Cp>Alright. So, we've got our subscription. We can see that come through. You know, if we create scores over here, 77. If we create these scores, we should not we'll still see those come through, which is great.\u003C/p>\u003Cp>You know, we need to start working on our board. Alright. So let's flush this out a little bit more. Yeah. Maybe we want to grab the let's just work on our leaderboard first.\u003C/p>\u003Cp>That's what we'll do. We'll get some UI rolling for this. I'm just gonna replace this down here at the bottom. So we'll just drop this text out. Great.\u003C/p>\u003Cp>Just totally disappeared. And then we'll do let's add a add a heading for this. P high scores. Class font mono, font bold. Make this look really, really nice.\u003C/p>\u003Cp>Yeah. Text 2 XL, Text center. Add some styling to it. See what we get. High score should be white.\u003C/p>\u003Cp>We want all this to be white. Text white. Alright. There's our high scores. We'll go in and add some padding.\u003C/p>\u003Cp>Oh, I don't wanna pad that. Let's add padding to this. Maybe we do p 6, p 8, something like that. Alright. And then let's do a list of our high scores.\u003C/p>\u003Cp>So we'll do a list v 4 scores, 5 scores, and 7 scores ago. This will be grid. Grid calls 4, and then we'll just do, this is gonna be our score. Let's say 5. These could be a span, I guess.\u003C/p>\u003Cp>I don't know what the rules are about putting a p tag inside there. Span, we got John Ross, then we've got the message, and then we've got a time stamp. 3 minutes ago. Alright. Let's see what that looks like.\u003C/p>\u003Cp>Of course, it's not displaying. Let's put that text as lime green. Make it lime green. We'll make them bold as well. Do font mono.\u003C/p>\u003Cp>Okay. Alright. And then we probably got well, actually, let's make this our header row. Right? So we got the score.\u003C/p>\u003Cp>Got the user, And then we have the time. Alright. As far as our scores, we will make those extra large. Score, message, time. Great.\u003C/p>\u003Cp>Good. Okay. So as far as our scores, right, we're gonna need an array of scores. So let's go up, and we'll just add that. So we got scores here.\u003C/p>\u003Cp>Get your scores here. There's an array of those particular scores. And how do we, on a knit, we want to get those scores. Alright? So I could potentially fetch those via the REST client, but, let's use our put our heads together for this, and we'll do, we'll just send a message.\u003C/p>\u003Cp>Right? So let's create a quick function. We'll we'll reuse the same connection. So we're creating a subscription there, but let's create get initial scores, get initial scores. It's great.\u003C/p>\u003Cp>And then we're gonna do this where we say, direct us dot send message. So we're gonna send a message via WebSocket connection. We're gonna use the items type. We're going to use our read action, and then we have our collection of scores. We want to limit that to 10 scores.\u003C/p>\u003Cp>That makes sense. And then maybe we add oh, I'm sorry. This needs to be wrapped in a query though. Limit 10. And then maybe we want to sort those by value.\u003C/p>\u003Cp>So we're gonna get initial scores, and what? Okay. We'll just call get initial scores. Let's see what we've got when we open this up. So there's our data.\u003C/p>\u003Cp>Right? Looks good. But and those are sorted in the correct order. Alright. So let's add a handler for this.\u003C/p>\u003Cp>So maybe we add a UID for this as well so we can keep track of these. These are gonna be the initial scores. Alright. So if the event dot UID equals initial scores, we're going to populate the scores dot value with event dot data. Is it just dot data?\u003C/p>\u003Cp>I think it should just be this. Right? Alright. So we open up our view dev tools. We go to arcade cabinet.\u003C/p>\u003Cp>We'll just see. We've got some scores. Cool. Alright. So now that we are getting those scores, let's just iterate through those scores.\u003C/p>\u003Cp>So we'll do the same thing with the list. And this one we're gonna add a v 4. So that'll be a score and scores. Key will be the score dot ID, not scores, just score dot ID. And then we'll start populating.\u003C/p>\u003Cp>Score dot value. Why does it keep auto completing on me? Score dot username score dot message. And then the score dot timestamp. Alright.\u003C/p>\u003Cp>So we got our scores. Those are a little huge, so maybe we shrink those down. And maybe this is actually we want the score to be larger. Tex x l. Cool.\u003C/p>\u003Cp>And maybe we actually make these small. Okay. Alright. So I do have a function already, like a helper function in this. It's called get relative time.\u003C/p>\u003Cp>It is the function. It just returns like a a relative time stamp. There we go. 8 minutes ago. Looks great.\u003C/p>\u003Cp>Got our messages. We got our username. We got our scores. That's great. So now if we anytime we load this page, we're we're getting those initial scores.\u003C/p>\u003Cp>And, you know, now we've got to like, how are we gonna submit our other scores? How are we doing on time here? We got about 40 minutes left. So we we probably wanna build a form at the end of the game to track those scores and and allow people to submit them. But one of the other things that I wanna do here is maybe we use a a computed prop to to show these as well.\u003C/p>\u003Cp>We only want the latest ten scores maybe. Or, you know, we we could show the whole list, but then we're gonna have to resort those scores as well. So this is it might be a good thing that the computer prop is for. So we'll do, what, compute, const equals sorted scores equals computed. Oh, there we go.\u003C/p>\u003Cp>GitHub Copilot to the rescue, so we're at scores dot value. And then maybe that's what we use to show. Sorted scores. Alright. Do we still get the same thing?\u003C/p>\u003Cp>That's great. Okay. Awesome. And what else are we going to do? Maybe we add just a little bit of gap.\u003C/p>\u003Cp>Gap y 2. Oh, no. It does. It's not gonna be a gap. Let's do this where we do, like, a space y 2 below them.\u003C/p>\u003Cp>Do we need a divider there? Maybe we do have a divider. Class equals text at lime 500. I don't think that's having the effect that I want. Okay.\u003C/p>\u003Cp>Regardless, there we go. We've got this function. Now we want to, whenever we play the game, right, we need to submit a score. So at the end here, I've got 0 emails. Maybe we add another button for our actual scores.\u003C/p>\u003Cp>So we're gonna go into the game itself. Right. And what are we gonna do here? We're gonna add a couple things. Right.\u003C/p>\u003Cp>We probably want to, the Kaboom library is based on Canvas, so we're not going to have like HTML elements there to work with. So I'm thinking we just maybe show a modal. I've got this Nuxt UI library included in this starter kit for this, And I do know that they have a little modal component that we could pop up. So let's roll with that and see what we got. Alright.\u003C/p>\u003Cp>So inside the game, you can see here's all our variables for that. But let's just a knit a couple of of different variables up here at the top. We've got a, maybe like a show form. That'll be reactive. And then we have, like, a, what are we gonna call this?\u003C/p>\u003Cp>I don't wanna call it score because Kaboom uses that internally. Maybe we just call this the player score, and we could use just like a reactive object for this instead of a ref. And so we have a value for the score, we'll set that to 0 by default. We've got a message for that and we've got a username. So this is gonna be the data that we submit, when the game is completed.\u003C/p>\u003Cp>Alright. So scroll down. We'll probably need a submit function for that as well. So we'll do a function. And we don't have to use async here because we're gonna reuse that same WebSocket connection.\u003C/p>\u003Cp>Right? So we got submit score. Let's go up here to the top. We're gonna get our directus client. Directus equals use Nuxt app from that plug in.\u003C/p>\u003Cp>And inside here, let's do do we do like a try catch? Just in case there are errors. Cool. We are going to submit a message. Direct us, send message.\u003C/p>\u003Cp>And here, the type is gonna be what? It will be items again. So we wanna send to the items. We're gonna do an action. I don't need to put that there.\u003C/p>\u003Cp>Do action of create. The collection is gonna be scores, and then our data will be the player score. Okay? And then after that, we would not show the form, and then, yeah, maybe here we'll just console dot error any errors that we receive. Alright.\u003C/p>\u003Cp>So that's the scaffold here for this. We're gonna need a template for this. So we'll have to add that modal to it. So we'll scroll down. Scroll down.\u003C/p>\u003Cp>And then we we're gonna need to actually show that somewhere as well. So Canvas. Let's go back into the arcade cabinet. Class p t. Actually, gonna do do I need this?\u003C/p>\u003Cp>Yes. Let's place that here. P t minus what was it? P t 24 fix. Alright.\u003C/p>\u003Cp>So I'm gonna move that here. And can I just stick this model in there? U model v model equals show form. And do we want we don't want the overlay for this either. So let's do false on the overlay.\u003C/p>\u003Cp>And we'll say form here. Alright. Does that mess with anything on our game? I don't think so. If we go into our game, we should see this variable somewhere in show form.\u003C/p>\u003Cp>We can check the box, change that variable. Nope. Not updating it. But regardless, should be good. Okay.\u003C/p>\u003Cp>Why why can't you edit that? True. Just always set to false. Weird. Alright.\u003C/p>\u003Cp>Regardless, what we're we're gonna do now is, at the end of this game, we need to add a button to submit our scores. So we're gonna find the end scene. Is it end? No. What is this gonna be?\u003C/p>\u003Cp>Countdown. Let's just start countdown. Okay. So we're looking for the lose scene. And here inside the lose scene, we've got a restart button.\u003C/p>\u003Cp>We've got a text inside that button. So I'm just gonna copy these items here. Come down. This is gonna be the submit high score, score button logic just so we can comment. Let's call this the submit score button Submit.\u003C/p>\u003Cp>Score button. Is it gonna be the same color button? Maybe we make it purple. I don't know what the RGB is for purple. Purple RGB.\u003C/p>\u003Cp>Let's see. Like a blue violet. Okay. We'll roll with that. Not sure that's exactly what I want, but okay.\u003C/p>\u003Cp>And then on the submit score button dot on click, we want to show form. Nobody ever calls me. Show form equals true for the value and then inside the actual text for this, we're gonna say submit score. And that will be where we've got that restart button. We're gonna do submit score button dotpos.\u003C/p>\u003Cp>Okay. Alright. So if we did everything correct, hopefully, this will work. And we'll be able to see the submit score button. Okay.\u003C/p>\u003Cp>It's now taking the place of our other button, but we need to fix that. So let's just move it out of the way. Submit score button. Let's move it to the other side so we could see this is width divided by 4. Let's do the width.\u003C/p>\u003Cp>We'll just do the width of the canvas minus width divided by 4. Is that gonna get us what we want? We'll just quickly lose and and test to make sure. Lots of distractions on this episode as well. Alright.\u003C/p>\u003Cp>So submit. We can see a form here. That's looking great. Let's wrap this inside a card. Ucard.\u003C/p>\u003Cp>K. And then we're gonna build a form based on this. So, Nuxt has this UI library has some of these form elements. We're going to use a form group. We'll give it a label.\u003C/p>\u003Cp>What are we going to have for the label? We'll have the username. And inside that we'll have an input. So u input v model player score dot username. Let's see what that looks like.\u003C/p>\u003Cp>Okay. That looks nice. So also inside this, maybe we have a p, submit your score. Old text x l. Submit your score.\u003C/p>\u003Cp>We got a username. And then we want to actually show the score as well. Right? So fontmonotext. Violet.\u003C/p>\u003Cp>500. You deduct what? Player score dot value emails. Alright. So we're only showing that we ducked we ducked, just 0 emails.\u003C/p>\u003Cp>Right? So we're probably gonna have to fix that. Maybe we don't even need that actually. We'll just show you doc 0 emails. Add some padding between these 2.\u003C/p>\u003Cp>Just using space y 4, just a little helper class. Alright. Is that giving me what I want? No. It's not.\u003C/p>\u003Cp>Something about the card space y 4. Okay. And then we're gonna have another form group for our message. Alright. We got our message.\u003C/p>\u003Cp>You input that's actually gonna be you text area, v model. K. And what else do we need? We need some buttons. Submit, u button, cancel.\u003C/p>\u003Cp>So we get 2 buttons. Let's make this size large. And this will be let's wrap this in a div. Just flex them, give a little bit of gap between the 2. And this will be variant, what, like a outline, maybe?\u003C/p>\u003Cp>That's good. P class. K. And then we're gonna I don't know what happened to our spacing. Let's add that back.\u003C/p>\u003Cp>Okay. So there's our form. We've got submit score. You deduct 0 emails. The other thing that we're gonna do here, if we submit this score let's just test it out and see.\u003C/p>\u003Cp>Right? Bryant Ross. Submit. Are we actually getting the scores? We are not getting the score.\u003C/p>\u003Cp>Is that score showing up though? We refresh indirectus. Okay. So we just see that score show up, but we're not populating that score to the scores array, which we need to sort. And then, also, if we submit the scores again, you can see we've got the same message.\u003C/p>\u003Cp>So we probably want to reset the message every time. So let's just do this, player score dot, message equals null. Okay. So whenever we score, submit that score. Good.\u003C/p>\u003Cp>What's the other thing that we need to do? We need to iterate this player score reactive value anytime we increase the score. So here's a score plus plus. What we're gonna do here is player score dot value equals the score. So after we iterate, we're going to plug that score, and then we need to actually populate our scores here as well.\u003C/p>\u003Cp>Right? So if we go back to our arcade cabinet, we need another event handler here. So if the event dot UID equals scores dot sub, then we want to push that event data into the scores array, and that should trigger what we want. But, this is actually gonna be an array, so we can't really push that into the array. We need to destructure that.\u003C/p>\u003Cp>So, thinking this should get us where we want to be. Event data is not iterable. Let's scope this down. Event dot event equals create. And this is confusing.\u003C/p>\u003Cp>Right? Let's swap this out. Message. Change this over to message so that that is a little less confusing. Okay.\u003C/p>\u003Cp>So now we're seeing those scores populate correctly. Let's just give this a shot and see what we got. Alright. We're facing off against John Ross. Duct a couple of emails.\u003C/p>\u003Cp>We'll close this out. We'll submit our score. You ducked 4 emails. This is gonna be John Ross. Hi, guys.\u003C/p>\u003Cp>And we submit, and now we can see that score being populated automatically. So that's pretty freaking awesome. Right? What else do we want to do with this? Maybe we animate it a little bit?\u003C/p>\u003Cp>One of the libraries that I like to use that is actually included in this one is, v auto animate. Or, well, it's not, v auto. That's the directive for this. But it is, auto animate by the the fine folks at FormKit. So one line of code, you know, you've all seen this before.\u003C/p>\u003Cp>You add logic to it. But this one, it auto animates for you, giving you kind of a nice UI. So if we try this again, test message. Yeah. Probably have to refresh the page after I save it.\u003C/p>\u003Cp>But there we go. We've got our scores populating. These should be sorted in order as well. What do we have time left on the clock? We've got 22 minutes left.\u003C/p>\u003Cp>You know, we could go through and build a dashboard for this, but let's just kinda recap where we're at. We've got the real time tracking of the scores. We've got to submit a username. We are displaying those high scores. Let's add some sound effects.\u003C/p>\u003Cp>Let's just test it out one more time, make sure this is actually working first. Alright. Duck. Alright. Got 6, 7.\u003C/p>\u003Cp>Let's just die right now at this moment. And this will be Bry Ross is my username. You stink John. And I take my place on the leaderboard. Cool.\u003C/p>\u003Cp>Alright. So calling out when, you know, sound effects, maybe we could go in those. I'm not sure you're gonna be able to hear them though on this recording. So as far as a real time leaderboard, I I think we're calling that good. That is finished.\u003C/p>\u003Cp>Pretty savvy. Where could we take this from here? Right? We could go a a lot of different directions with this. You know, expanding this for, like, user authentication, maybe pull in, like, a GitHub username, something like that.\u003C/p>\u003Cp>But I'm pretty comfortable with this. I think I'm going to button this up and we'll probably actually ship this to the live version of this at directus. Is/arcade. I think this will look nice with a leaderboard on here. So that's it for this episode of 100 Apps, 100 Hours.\u003C/p>\u003Cp>I think this may be the shortest episode yet. That's it though. That's good. In and out. I hope to see you on the next one.\u003C/p>\u003Cp>Bye.\u003C/p>","Hi. Welcome back to another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, and today we are building something that I think is pretty cool. But here on 100 Apps 100 Hours, we build your suggestions, your favorite apps, or rebuild a clone of some other app in 1 hour or less, or publicly fail, get humiliated, trying. It was a lot of stress, to be honest with you. So the rules for this, number 1, we have 60 minutes to plan and build an application, no more, no less. And number 2, just use whatever you have at your disposal. So, could be AI, could be past projects, whatever we've got. We're just trying to speed run through applications. And today we've got a cool one. We're going to be building a real time leaderboard. So recently, as far as, like April Fools' Day, we put out this amazing game to make you a duckin' pro at cold email. So it's called duckin' cold emails. It's just a runner game where you face off against a sales rep who is sending you cold emails and pop ups on the screen here. So you duck or jump, you get a couple of lives, and at the end of it you can choose to play again, and you get a nice little score. So what we're going to do here is create a leaderboard for this, and riggity jig, away we go. Alright. So, 60 minutes to plan and build. Hopefully we don't need all of that, but maybe we will. That's how this works. Alright. So I usually like to start by mocking out the functionality that we need out of this. So what do I really want to happen here? When I play, I wanna be able to submit my score and if somebody else is playing around the world, I want that score to and they get a high score, I want that to pop up. I wanna see that. So we've got a real time tracking of high scores. This is really large. Let's just trim that down. Real time tracking of high scores, submit a username and message with that, maybe? Maybe we'll make this a little more competitive and that we can talk trash to each other while we do it. What else is there really? Display those scores. Display high scores. I'm gonna put maybe sound effects. I'm gonna put that in question mark. Maybe that'll be our stretch goal. So that's that's basically the functionality of this. Right? I don't know explicitly how long that's gonna take, but what do we have as far as a data model? That's one of the other things that I like to map out. We've got some actual players, but, you know, I'm not going to have a user that needs to log in for this. I'm just going to let we're going to do it NBA Jam style, where you can just key in your initials or something like that. So I'm just thinking here, there's probably only maybe just a single table for this, which is rare for us on a 100 apps to a 100 hours. We're going to have a value for the score, a username, there's gonna be an ID and a time stamp of of when that high score was, and then maybe a message. That seems like it's it to me. I don't wanna make this over complicated. Alright. So I'm gonna go into my direct assistance. This is totally blank. We're gonna create a new table. We're gonna call it scores. Can I zoom in on this? Yeah. There we go. We'll make it really fancy. We'll do the generated UUID. We've got date created. So this is a system field or optional system field that will basically whenever this item gets created it will pre populate some stuff for me. So basically what it does on create of this record in this collection, it will save the current date and time. So we'll unhide this because I do want to see that, And then we'll add a value. So as far as the number of emails ducked, I can't duck half an email. So that's gonna be an integer. Looks great. What else do we have? Maybe we'll make that half width. And a username. So go sweet handle. Cool. And then we're gonna have a message. So we'll just use the text area for that. The type is text, of course, so I could add, like, markdown or something for that if I wanted to, but we'll just keep this short and sweet. Alright. So as far as our data model, is that that's probably it. Right? Looks good. Getting on my toes here for this one. Alright. So we got this, specific application. It's basically just a Nuxt single page application. If I look, I've got a single index page, there's an arcade cabinet component that is doing most well, it's not really doing hardly any of the work it's just presentation. It gives us this nice arcade cabinet and some text down below. And then we also have oh, these were my buttons I was trying the last time I was in here. And then we have a game component. So the library that we're using for the actual game here is pretty interesting itself. You might go ahead and check it out if you are trying to build a game. Did we start the time? Oh, yeah. We did start the timer. Okay. It's called Kaboom JS. A pretty interesting library, actually. Makes it really easy to build quick little mini games like this, build stuff that's fun for your users. Alright. So that's the meat and potatoes of it. You know, there's some sprites, there's some functions, there's a couple of different scenes inside the game, but that's what we've got, right? So we've got our scores, let's go about setting up our real time connection. So Directus has real time baked into the platform. If I pull up my other project that I'm actually connected to, you can see I have enabled my WebSockets here. And away we go. Alright, so as far as the Knox application, I've just got a simple Directus plug in that provides me with a real time client. That's really all we've got. I've set the auth mode to public just so I've got, you know, anybody can connect to this. Again, we're not going to make authentication required here. What I'm also going to do is the ability to read scores and push scores. Okay. So what else do we have? I think that's pretty much it. Right? Let's dive in. Let's go to our arcade cabinet. Pull this up. Right. This is our wrapper component. I'm not sure where I'm gonna stick the leaderboard. Maybe up here at the top, maybe somewhere down here at the bottom. We could just cannibalize this bottom section down here. I'll zoom out in a little bit. But we're gonna get started with the SDK and WebSockets. So we've got a great guide on the documentation for this. It walks you through this in great detail. But we're just gonna pull this up. I'm gonna pull that Directus client. Direct cuts. Directus. I can't spell. Alright. We're gonna use the Nuxt app. Use Nuxt app composable to get that client. And then what we're gonna do is await directus dot connect. Alright. So that will connect and if we keep scrolling down, we can see that we've got a, open function here. You know, if we just wanna log that this was opened. We do this and we go back. And if I look in the console, should be getting a message that this was opened. Switching protocols. Not seeing any messages in here for whatever reason. Yeah. I don't see any particular messages, but let's keep moving on. We probably do need this directus dot message handler. Directus dot on web socket dot message console log events. What if we just log the entire event? Actually, that'll be a message. Same thing. Let's see what we get here. Do we have that connection established? Where is our connection? Why are we not seeing any messages through here? Right? Okay. Let's just add a subscription as well and see what we got. Oh, could it be our permission settings? Right? Let's log in and see. Directory's password. Alright. So if we look at no. We've got access to that. Alright. We've solved that problem already. Alright. So let's just create a subscription and see if we get that. Alright. So we'll go in and do directus dot subscribe, and we're gonna subscribe to the scores, And we're gonna get, we're gonna pass some options to this. So we want the event to be create. The query do we actually need a query? I don't think we let's not limit ourselves at this moment. And then maybe I wanna add, like, a UID to this, and we're gonna call this the scores sub. Subscription direct us dot subscribe. And let's see what we got here. Okay. So now I can see that particular message coming through. There's our subscription for this. So we're actually getting some data. Let's take a look and see if we open this up in a separate window. So we'll just open scores up. We'll create a new scores item, see if we get subscription for that. Yep. Okay. I see some data coming through. Great. So we are getting some data for our scores, which is good. Not sure why it's not showing the open function. Not really concerned with that, anyway. Alright. So what we wanna do, just one moment, please. We'll stop the clock. Okay. Had a hot call come in. We're restarting the clock. No extra work has been done, and I've totally lost track of where we were at in the process anyway. Alright. So, we've got our subscription. We can see that come through. You know, if we create scores over here, 77. If we create these scores, we should not we'll still see those come through, which is great. You know, we need to start working on our board. Alright. So let's flush this out a little bit more. Yeah. Maybe we want to grab the let's just work on our leaderboard first. That's what we'll do. We'll get some UI rolling for this. I'm just gonna replace this down here at the bottom. So we'll just drop this text out. Great. Just totally disappeared. And then we'll do let's add a add a heading for this. P high scores. Class font mono, font bold. Make this look really, really nice. Yeah. Text 2 XL, Text center. Add some styling to it. See what we get. High score should be white. We want all this to be white. Text white. Alright. There's our high scores. We'll go in and add some padding. Oh, I don't wanna pad that. Let's add padding to this. Maybe we do p 6, p 8, something like that. Alright. And then let's do a list of our high scores. So we'll do a list v 4 scores, 5 scores, and 7 scores ago. This will be grid. Grid calls 4, and then we'll just do, this is gonna be our score. Let's say 5. These could be a span, I guess. I don't know what the rules are about putting a p tag inside there. Span, we got John Ross, then we've got the message, and then we've got a time stamp. 3 minutes ago. Alright. Let's see what that looks like. Of course, it's not displaying. Let's put that text as lime green. Make it lime green. We'll make them bold as well. Do font mono. Okay. Alright. And then we probably got well, actually, let's make this our header row. Right? So we got the score. Got the user, And then we have the time. Alright. As far as our scores, we will make those extra large. Score, message, time. Great. Good. Okay. So as far as our scores, right, we're gonna need an array of scores. So let's go up, and we'll just add that. So we got scores here. Get your scores here. There's an array of those particular scores. And how do we, on a knit, we want to get those scores. Alright? So I could potentially fetch those via the REST client, but, let's use our put our heads together for this, and we'll do, we'll just send a message. Right? So let's create a quick function. We'll we'll reuse the same connection. So we're creating a subscription there, but let's create get initial scores, get initial scores. It's great. And then we're gonna do this where we say, direct us dot send message. So we're gonna send a message via WebSocket connection. We're gonna use the items type. We're going to use our read action, and then we have our collection of scores. We want to limit that to 10 scores. That makes sense. And then maybe we add oh, I'm sorry. This needs to be wrapped in a query though. Limit 10. And then maybe we want to sort those by value. So we're gonna get initial scores, and what? Okay. We'll just call get initial scores. Let's see what we've got when we open this up. So there's our data. Right? Looks good. But and those are sorted in the correct order. Alright. So let's add a handler for this. So maybe we add a UID for this as well so we can keep track of these. These are gonna be the initial scores. Alright. So if the event dot UID equals initial scores, we're going to populate the scores dot value with event dot data. Is it just dot data? I think it should just be this. Right? Alright. So we open up our view dev tools. We go to arcade cabinet. We'll just see. We've got some scores. Cool. Alright. So now that we are getting those scores, let's just iterate through those scores. So we'll do the same thing with the list. And this one we're gonna add a v 4. So that'll be a score and scores. Key will be the score dot ID, not scores, just score dot ID. And then we'll start populating. Score dot value. Why does it keep auto completing on me? Score dot username score dot message. And then the score dot timestamp. Alright. So we got our scores. Those are a little huge, so maybe we shrink those down. And maybe this is actually we want the score to be larger. Tex x l. Cool. And maybe we actually make these small. Okay. Alright. So I do have a function already, like a helper function in this. It's called get relative time. It is the function. It just returns like a a relative time stamp. There we go. 8 minutes ago. Looks great. Got our messages. We got our username. We got our scores. That's great. So now if we anytime we load this page, we're we're getting those initial scores. And, you know, now we've got to like, how are we gonna submit our other scores? How are we doing on time here? We got about 40 minutes left. So we we probably wanna build a form at the end of the game to track those scores and and allow people to submit them. But one of the other things that I wanna do here is maybe we use a a computed prop to to show these as well. We only want the latest ten scores maybe. Or, you know, we we could show the whole list, but then we're gonna have to resort those scores as well. So this is it might be a good thing that the computer prop is for. So we'll do, what, compute, const equals sorted scores equals computed. Oh, there we go. GitHub Copilot to the rescue, so we're at scores dot value. And then maybe that's what we use to show. Sorted scores. Alright. Do we still get the same thing? That's great. Okay. Awesome. And what else are we going to do? Maybe we add just a little bit of gap. Gap y 2. Oh, no. It does. It's not gonna be a gap. Let's do this where we do, like, a space y 2 below them. Do we need a divider there? Maybe we do have a divider. Class equals text at lime 500. I don't think that's having the effect that I want. Okay. Regardless, there we go. We've got this function. Now we want to, whenever we play the game, right, we need to submit a score. So at the end here, I've got 0 emails. Maybe we add another button for our actual scores. So we're gonna go into the game itself. Right. And what are we gonna do here? We're gonna add a couple things. Right. We probably want to, the Kaboom library is based on Canvas, so we're not going to have like HTML elements there to work with. So I'm thinking we just maybe show a modal. I've got this Nuxt UI library included in this starter kit for this, And I do know that they have a little modal component that we could pop up. So let's roll with that and see what we got. Alright. So inside the game, you can see here's all our variables for that. But let's just a knit a couple of of different variables up here at the top. We've got a, maybe like a show form. That'll be reactive. And then we have, like, a, what are we gonna call this? I don't wanna call it score because Kaboom uses that internally. Maybe we just call this the player score, and we could use just like a reactive object for this instead of a ref. And so we have a value for the score, we'll set that to 0 by default. We've got a message for that and we've got a username. So this is gonna be the data that we submit, when the game is completed. Alright. So scroll down. We'll probably need a submit function for that as well. So we'll do a function. And we don't have to use async here because we're gonna reuse that same WebSocket connection. Right? So we got submit score. Let's go up here to the top. We're gonna get our directus client. Directus equals use Nuxt app from that plug in. And inside here, let's do do we do like a try catch? Just in case there are errors. Cool. We are going to submit a message. Direct us, send message. And here, the type is gonna be what? It will be items again. So we wanna send to the items. We're gonna do an action. I don't need to put that there. Do action of create. The collection is gonna be scores, and then our data will be the player score. Okay? And then after that, we would not show the form, and then, yeah, maybe here we'll just console dot error any errors that we receive. Alright. So that's the scaffold here for this. We're gonna need a template for this. So we'll have to add that modal to it. So we'll scroll down. Scroll down. And then we we're gonna need to actually show that somewhere as well. So Canvas. Let's go back into the arcade cabinet. Class p t. Actually, gonna do do I need this? Yes. Let's place that here. P t minus what was it? P t 24 fix. Alright. So I'm gonna move that here. And can I just stick this model in there? U model v model equals show form. And do we want we don't want the overlay for this either. So let's do false on the overlay. And we'll say form here. Alright. Does that mess with anything on our game? I don't think so. If we go into our game, we should see this variable somewhere in show form. We can check the box, change that variable. Nope. Not updating it. But regardless, should be good. Okay. Why why can't you edit that? True. Just always set to false. Weird. Alright. Regardless, what we're we're gonna do now is, at the end of this game, we need to add a button to submit our scores. So we're gonna find the end scene. Is it end? No. What is this gonna be? Countdown. Let's just start countdown. Okay. So we're looking for the lose scene. And here inside the lose scene, we've got a restart button. We've got a text inside that button. So I'm just gonna copy these items here. Come down. This is gonna be the submit high score, score button logic just so we can comment. Let's call this the submit score button Submit. Score button. Is it gonna be the same color button? Maybe we make it purple. I don't know what the RGB is for purple. Purple RGB. Let's see. Like a blue violet. Okay. We'll roll with that. Not sure that's exactly what I want, but okay. And then on the submit score button dot on click, we want to show form. Nobody ever calls me. Show form equals true for the value and then inside the actual text for this, we're gonna say submit score. And that will be where we've got that restart button. We're gonna do submit score button dotpos. Okay. Alright. So if we did everything correct, hopefully, this will work. And we'll be able to see the submit score button. Okay. It's now taking the place of our other button, but we need to fix that. So let's just move it out of the way. Submit score button. Let's move it to the other side so we could see this is width divided by 4. Let's do the width. We'll just do the width of the canvas minus width divided by 4. Is that gonna get us what we want? We'll just quickly lose and and test to make sure. Lots of distractions on this episode as well. Alright. So submit. We can see a form here. That's looking great. Let's wrap this inside a card. Ucard. K. And then we're gonna build a form based on this. So, Nuxt has this UI library has some of these form elements. We're going to use a form group. We'll give it a label. What are we going to have for the label? We'll have the username. And inside that we'll have an input. So u input v model player score dot username. Let's see what that looks like. Okay. That looks nice. So also inside this, maybe we have a p, submit your score. Old text x l. Submit your score. We got a username. And then we want to actually show the score as well. Right? So fontmonotext. Violet. 500. You deduct what? Player score dot value emails. Alright. So we're only showing that we ducked we ducked, just 0 emails. Right? So we're probably gonna have to fix that. Maybe we don't even need that actually. We'll just show you doc 0 emails. Add some padding between these 2. Just using space y 4, just a little helper class. Alright. Is that giving me what I want? No. It's not. Something about the card space y 4. Okay. And then we're gonna have another form group for our message. Alright. We got our message. You input that's actually gonna be you text area, v model. K. And what else do we need? We need some buttons. Submit, u button, cancel. So we get 2 buttons. Let's make this size large. And this will be let's wrap this in a div. Just flex them, give a little bit of gap between the 2. And this will be variant, what, like a outline, maybe? That's good. P class. K. And then we're gonna I don't know what happened to our spacing. Let's add that back. Okay. So there's our form. We've got submit score. You deduct 0 emails. The other thing that we're gonna do here, if we submit this score let's just test it out and see. Right? Bryant Ross. Submit. Are we actually getting the scores? We are not getting the score. Is that score showing up though? We refresh indirectus. Okay. So we just see that score show up, but we're not populating that score to the scores array, which we need to sort. And then, also, if we submit the scores again, you can see we've got the same message. So we probably want to reset the message every time. So let's just do this, player score dot, message equals null. Okay. So whenever we score, submit that score. Good. What's the other thing that we need to do? We need to iterate this player score reactive value anytime we increase the score. So here's a score plus plus. What we're gonna do here is player score dot value equals the score. So after we iterate, we're going to plug that score, and then we need to actually populate our scores here as well. Right? So if we go back to our arcade cabinet, we need another event handler here. So if the event dot UID equals scores dot sub, then we want to push that event data into the scores array, and that should trigger what we want. But, this is actually gonna be an array, so we can't really push that into the array. We need to destructure that. So, thinking this should get us where we want to be. Event data is not iterable. Let's scope this down. Event dot event equals create. And this is confusing. Right? Let's swap this out. Message. Change this over to message so that that is a little less confusing. Okay. So now we're seeing those scores populate correctly. Let's just give this a shot and see what we got. Alright. We're facing off against John Ross. Duct a couple of emails. We'll close this out. We'll submit our score. You ducked 4 emails. This is gonna be John Ross. Hi, guys. And we submit, and now we can see that score being populated automatically. So that's pretty freaking awesome. Right? What else do we want to do with this? Maybe we animate it a little bit? One of the libraries that I like to use that is actually included in this one is, v auto animate. Or, well, it's not, v auto. That's the directive for this. But it is, auto animate by the the fine folks at FormKit. So one line of code, you know, you've all seen this before. You add logic to it. But this one, it auto animates for you, giving you kind of a nice UI. So if we try this again, test message. Yeah. Probably have to refresh the page after I save it. But there we go. We've got our scores populating. These should be sorted in order as well. What do we have time left on the clock? We've got 22 minutes left. You know, we could go through and build a dashboard for this, but let's just kinda recap where we're at. We've got the real time tracking of the scores. We've got to submit a username. We are displaying those high scores. Let's add some sound effects. Let's just test it out one more time, make sure this is actually working first. Alright. Duck. Alright. Got 6, 7. Let's just die right now at this moment. And this will be Bry Ross is my username. You stink John. And I take my place on the leaderboard. Cool. Alright. So calling out when, you know, sound effects, maybe we could go in those. I'm not sure you're gonna be able to hear them though on this recording. So as far as a real time leaderboard, I I think we're calling that good. That is finished. Pretty savvy. Where could we take this from here? Right? We could go a a lot of different directions with this. You know, expanding this for, like, user authentication, maybe pull in, like, a GitHub username, something like that. But I'm pretty comfortable with this. I think I'm going to button this up and we'll probably actually ship this to the live version of this at directus. Is/arcade. I think this will look nice with a leaderboard on here. So that's it for this episode of 100 Apps, 100 Hours. I think this may be the shortest episode yet. That's it though. That's good. In and out. I hope to see you on the next one. Bye.","06ce65f4-331e-4797-8158-82b1d29dee97",[455],"765f110e-fcec-422a-b672-2ebb1f8830d6",[],{"id":157,"number":158,"show":122,"year":159,"episodes":458},[161,162,163,164,165,166,167,168,169,170],{"id":165,"slug":460,"vimeo_id":461,"description":462,"tile":463,"length":285,"resources":8,"people":8,"episode_number":270,"published":464,"title":465,"video_transcript_html":466,"video_transcript_text":467,"content":8,"seo":468,"status":130,"episode_people":469,"recommendations":471,"season":472},"remote-job-board","936433174","Race right along with Bryant as he tackles building a virtual job board inspired by We Work Remotely. He tries to build a ton of functionality in just 60 mins – job listings, integrating categories and companies, and job post submissions with Directus and Nuxt.","c9c86637-29d7-4b38-b798-2a5a52ac2def","2024-05-17","Mission: Remote Job Board","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 Apps, 100 Hours. I'm your host Brian Gillespie and on the show we build some of your favorite apps or some of your suggestions or just random stuff that I've come across on the Internet in 1 hour or less or get publicly shamed trying when we fail. And I fail often. That's just the rub of this format.\u003C/p>\u003Cp>The rules are there are 60 minutes to plan and build the application, no more, no less. Sweating just thinking about it. But rule number 2 is use whatever you have at your disposal. AI, GitHub Copilot, tailwind CSS, tailwind UI, any pre made components, whatever we can to get the job done in 1 hour. Today, we are going to be building a job board.\u003C/p>\u003Cp>So the giant elephant in the room in this space is obviously Indeed. They are one of the largest job boards. Yeah. It's been a while since I've been on their site. The experience is not great, honestly.\u003C/p>\u003Cp>Maybe everybody's working on a a mobile or something, but, I I think the the job board that we're gonna model after today is we work remotely. We are an all remote team here at Directus, so this speaks to my heart. I've been working remotely for, the better part of a decade now. So, we are going to model after this. Let's dive in and get started, shall we?\u003C/p>\u003Cp>Alright. So we'll put 60 minutes on the clock. Let's just talk through the functionality that we're looking for here. And As I pull up the WeWork Remotely job board, what's our functionality? Right?\u003C/p>\u003Cp>We want to, obviously show a list of jobs. In this case they are sorted by category. We want to let someone let users post a job for a fee. That's nice. And, you know, this one's got subscribers as well, which seems like something that is relatively simple.\u003C/p>\u003Cp>There's a lot of moving pieces to that. So let's add a question mark and we'll do subscribe to job posts. Now I think this one does not allow you to apply within the platform. I think it just, like, sends you off to a third party thing. So that's the way we'll play this, you know, if you wanted to build like an application processing system, you could totally do that with Directus.\u003C/p>\u003Cp>That's a lot of ground to cover in 60 minutes though. So let's take a look at this, let's flesh out our actual data model, right? That's part of the planning process, so I'm just going to drag some boxes over here inside Figma. What are we going to have? We're going to have job posts or we could call it job listings.\u003C/p>\u003Cp>That's great. Within that, we're gonna have a few different things. Alright. What do we got? We got some tags, it looks like.\u003C/p>\u003Cp>So we got a category. We got a title for that post. We've got some content, we've got a link, we've got a like, a company. I guess if you're going to have a company, that would probably be here. That'd be a separate table.\u003C/p>\u003Cp>Right? There's a company. Company has a name, a website. Name. Website.\u003C/p>\u003Cp>What? Logo. Great. K. What else are we gonna have?\u003C/p>\u003Cp>If we make it 2 subscribers, we'll worry about that when we come across it, but let's dive in. Right? This seems pretty good. We'll just draw some arrows to make it look fancy. Right?\u003C/p>\u003Cp>A company has multiple job listings. One listing has one company. Alright. Great. Let's talk about what we're working with.\u003C/p>\u003Cp>So as far as applications, I've got a direct assistance spun up, it is blank. I've got a bare bones Nuxt app that has some login and register capability. Looks like I'm already logged in though, so all good there. And as far as the Directus communication, I've already got a Nuxt plug in defined just to talk to Directus a little bit easier, make it a little less boring on this specific episode. Alright.\u003C/p>\u003Cp>So let's start by I'm going to pull this up side by side with remote. Oh, no. That's not what we want. I could spend the whole time just messing with the Arc Browser. Alright.\u003C/p>\u003Cp>So we're talking about job listings. We're going to go ahead and add that as a new collection. And I think I can zoom in a little bit to make that more visible. And then we'll look at the actual listings that we have. Right?\u003C/p>\u003Cp>One of the other things that I do often is, I'm not sure how this site is put together, but, you know, on sites that are server side rendered, you can go into the actual code and especially if it's, like, a Next application or, I'm not sure if Nuxt does this or not, but you can actually see the data that's posted. I'm not sure if that's the case here. I see Jquery. This is probably server rendered from an actual server. No big deal.\u003C/p>\u003Cp>But, just one way you can kind of see what other folks are doing on the website, always inspect the code. I live in the JavaScript developer tools console. Alright, so we're going to have a job listing. We're going to have a pretty slug for all of these. So let's see what we're going to do, we're going to have a draft and publish listings, probably got a date created, did the user created, the date updated, etcetera.\u003C/p>\u003Cp>And let's go through and model out our listing, right. So we've got a title for the listing, we're going to have that. We've got to have a slug for the listing. So we'll just do a slug for the listing. We'll come back to that one in a moment.\u003C/p>\u003Cp>What else are we going to add? We're going to have some content. I see there's tags here. I don't know exactly how they're using the tags, but it looks like the the tags get their own page. So in that case, it makes me think we need, like, a relationship there.\u003C/p>\u003Cp>Let's look at that. We'll do a mini to mini relationship because one post could have many tags and there could be many tags to other posts. Alright, So we got tags as a key. I'm going to enter tags as a related collection here, right? And if I open up the advanced field mode inside Directus and I scroll down to the bottom, you'll see that, it's actually going to create a new table for me.\u003C/p>\u003Cp>So that tags table that I don't have, it's going to create it for me, which is kind of nice, right? And I can add this to the tags, I can show the job listings on the tags that have that specific tag. And maybe we can sort these as well. Cool. Alright.\u003C/p>\u003Cp>So we'll hit save. We've got tags. We also should have a tags collection now, which is nice. We have a junction table for those as well. Then we're gonna need some content.\u003C/p>\u003Cp>Right? We need the actual, content for this. That's gonna be the WYSIWYG editor. Is that great? Cool.\u003C/p>\u003Cp>The type will be text, that's what it's going to be saved as in the database. I can always adjust my toolbar if I need to. And, what else do we need really? Apply for this position, right? This is going to be a URL.\u003C/p>\u003Cp>So apply URL, Application URL. That's what we'll roll with. We're probably going to need a category for this as well. So if I go back to all the jobs, we can see we've got sales and marketing. If I zoom out a little bit, we can see all the different categories up here at the top, Programming, Design, etcetera.\u003C/p>\u003Cp>So we're going to create a new relationship inside Directus. Let's call it category. And we will again create a new collection here. So if, even if this doesn't exist, Directus is going to create this for me, which is, you know, super. It's a really easy, super nice way to do our data modeling.\u003C/p>\u003Cp>We can add the different job listings to the categories if we want to. In this case, maybe I don't need to show that. But we will, we don't have anything to adjust as far as the interface because this is not created yet, but we'll just go through the process. So we've got a category for this, We've got tags. And if I go back to my data, I'll see categories here now.\u003C/p>\u003Cp>So we've got categories, we've got tags. Great. What else are we missing on this? Is this a featured post or not? I'm assuming that's part of the checkout process.\u003C/p>\u003Cp>So, you know, we could add a Boolean tag for that is featured. I always like when I'm doing my keys, especially if they're Boolean prefix with the is. There's just a nice little standard naming convention. And on my interface, maybe the label is featured post. Great.\u003C/p>\u003Cp>Okay. Alright. So this looks pretty good, I want to say. Alright. We'll make some of these things half width.\u003C/p>\u003Cp>Maybe we make the title full width. We got the status. Actually, let's do this. We got status, we got title. Let's put slug down below title.\u003C/p>\u003Cp>And don't sleep on the Directus marketplace. Right? One of the extensions that I've been using a lot here lately is the WP slug interface extension. If you're used to WordPress, you got, like, this nice little slug interface there. We're just gonna install this, give it a quick refresh.\u003C/p>\u003Cp>Then I go back to my data model, I go to my slug, go to the interface, and I can switch this up to my slug. I get a template for this, so we're going to populate the title. I could potentially prefix this or, you know, auto generate it as well. So I'm just going to do On Create because after a thing is created, it's been published, changing the slug results in a different URL on your front end, could result in some nasty 404 errors. We don't want that.\u003C/p>\u003Cp>40 fours get out of here. Alright. So we got a job listing category. We've got did I\u003C/p>\u003Cp>Speaker 1: do job listing instead of job listings?\u003C/p>\u003Cp>Speaker 0: I did. Man. Alright. How can we fix that? We're not going to bother with it.\u003C/p>\u003Cp>I could dig into the SQL and adjust that if I needed to, but we're not going to worry too much with it right now. Typically, I keep my table names plural. Just a personal preference. Alright. So we've got a category.\u003C/p>\u003Cp>Let's go ahead and flesh out these categories. We need a title for the category. We probably need a slug for the category as well. So I can go back into my job listings, and if I want, I can duplicate this field to that collection. So we'll just change this to slug.\u003C/p>\u003Cp>We'll go to categories. Great. And in that case, do we actually want the tags to have their own page as well? The tag will have a title and the tag will probably have a slug as well. So we'll just copy that across.\u003C/p>\u003Cp>Slug. Change the collection to tags. And away we go. Right? The last thing that we talked about is a company.\u003C/p>\u003Cp>So we want to be able to select a company for our post and you know potentially a company could post 3 or 4 different times. Maybe they're hiring for a lot of different positions. So let's create a new table. We're going to call it companies. We'll do a generated UUID.\u003C/p>\u003Cp>Do we need all this information? Probably not. Alright. So we'll give the company name. And whatever I choose to name this key is totally up to me.\u003C/p>\u003Cp>And then we've got the website. That's just going to be a string. And then we're going to have a logo for that. Right? Logo.\u003C/p>\u003Cp>Alright. Cool. So now we've got it's just a basic data model set up. Let's go in and I'm just going to, like, just rip some of this content. We work remotely.\u003C/p>\u003Cp>Sorry for just sealing information off your job board here. Let's look at our different categories. So we'll just mop up a couple of these categories really quickly. We've got what? Programming?\u003C/p>\u003Cp>Oh, gosh, I can't spell. Programming, save. We've got Design, save. What else do we have? Sales and marketing.\u003C/p>\u003Cp>Sales and marketing. And what happens if we do this? Sales and? Oh, that's kind of a nice effect. Sales and marketing.\u003C/p>\u003Cp>Maybe we change this to just be sales dash marketing. Customer support. Customer support. Great. Okay.\u003C/p>\u003Cp>And this is all remote, so, you know, what are we gonna call this thing? Maybe we'll call it it, Dan's Remote Job Board after some of our own teammates here at Directus. Alright. So we've got some categories. Let's take a look at the tags within here.\u003C/p>\u003Cp>We've got full time. We've got, part time will be a tag. Right? Part time. What?\u003C/p>\u003Cp>SEO? Link building, WordPress, technical writing. What do we have on the development side? So we got programming. We got full stack, front end.\u003C/p>\u003Cp>What kind of tags do we have here? Europe only, etcetera. Hang on just a moment. I got an important call coming in. Be right Alright.\u003C/p>\u003Cp>So now we're back. We've got 46 minutes on the clock. Let's dive in further. We've got some tags. We've got like Europe only, etcetera.\u003C/p>\u003Cp>Great. Okay. So, those are a few tags. We've got some categories. We've got, let's see if there's any companies that we recognize on here for full stack development.\u003C/p>\u003Cp>I don't really recognize any of these. Right? Let's just create a new company here. We'll call this Directus HTTP Directus dot io. And we'll add the logo for this.\u003C/p>\u003Cp>Right? See what I got handy. Directus logo. We'll get this a couple of different ways. Logotype.\u003C/p>\u003Cp>That looks good. That's what we will roll with. We got the logotype. Maybe we add another company, but for now let's just add our first listing. Right.\u003C/p>\u003Cp>I'm just going to steal this one, full stack. Alright. We are going to make sure this is published. Is this a featured post? Looks like it is.\u003C/p>\u003Cp>Alright. We'll go in. And I could just copy this wholesale. And I'm thinking it should retain some of the content, some of the settings. Yeah.\u003C/p>\u003Cp>Okay. So it should retain the formatting for it. And then we're going to have a link for the application. And the last thing that we need to do is pick a category. And we probably need to add a company for this as well.\u003C/p>\u003Cp>And I I joked, it's not the last thing we need to do. We're going to add a few more. This is going to be a Europe only job. Be a $100,000 or more USD. That's that's kind of a nice tag.\u003C/p>\u003Cp>Let's do React as a tag. Oh, sorry. Creating an item in a tag. React. Great.\u003C/p>\u003Cp>Alright. Is this a featured post? Yes, it is. Let's save that post and then we'll go in and add a relationship to that company. So that'll be a many to one relationship inside Directus.\u003C/p>\u003Cp>We're gonna call this company because there's a single company, and then we have companies. And, you know, we could go in and add the company name here so that it displays nicely. We probably want to do the same thing for our categories, like give them a title, make sure those show on the interface in the display template. Same thing for tags. For our list view here, we're going to do the tags ID dot title and we'll do the same thing here.\u003C/p>\u003Cp>Great. Okay. So now if I open up this job, we can see our different tags for this. We've got our category. We've got the company that this is created for.\u003C/p>\u003Cp>Sweet. Alright. So now we've got a job listing. We've got some categories, we've got some tags, we've got some companies. Does a company going to have a slug?\u003C/p>\u003Cp>You know, potentially. Do they have a listing here? They do. So probably need a slug for the company as well. Slug.\u003C/p>\u003Cp>The template is going to be the name of the company. Great. Alright. So we've got our data model for this, right? The next thing that I'm going to do is just go through our access control and create this.\u003C/p>\u003Cp>So there are 2 roles that are out of the box inside Directus. I have public, I have administrator. I'm going to go through and, just basically set read access to all these. And then I can go back in and adjust this if I need to as well. So we're going to adjust the item permissions just a little bit to where we can only see published posts.\u003C/p>\u003Cp>So I'll go through listing tags, categories. I don't know the categories. Do we do publish for categories? We did not. Tags, etcetera.\u003C/p>\u003Cp>Yeah. So the rest of those are fine as is. Right? People can see all the companies, all the tags, all the categories. But if it's not a published listing, they can't see that information.\u003C/p>\u003Cp>Alright. So great. Let's go in and now, is it time to actually start fleshing something out inside our Nuxt application? Let's let's take a look at that. Right?\u003C/p>\u003Cp>So we've got here on the how do I separate these 2? Alright. Cool. So, Nuxt application on our pages directory, we've got an index page. This is probably going to be just our listing of posts.\u003C/p>\u003Cp>Right? So we can start fleshing this out a little bit. Let's remove this centering. That should move it way up here to the top. We'll just add a header for this.\u003C/p>\u003Cp>And what are we going to have in the header? We're going to flex. We are going to do Dan's remote job board. That's the name of this thing. We'll make sure it's like a mono font for Dan.\u003C/p>\u003Cp>I know how Dan likes this stuff. Okay. Let's give this header a little padding. PX6 PY. Alright.\u003C/p>\u003Cp>And we can even make this like sticky at the top if we want to. Sticky, top 0. Give it a background white. BG opacity. We do that backdrop blur effect.\u003C/p>\u003Cp>One of the nice things about using tailwind, backdrop blur, BG opacity. I could actually just adjust it this way. Let's do dash or slash 50. And cool. Alright.\u003C/p>\u003Cp>So now we've got a header. We might have some links inside our header that we'll cover in just a moment. Let's do a div to ul. Links. Do we actually need these on a list?\u003C/p>\u003Cp>Probably not. Let's do what? Nuxt link v 4, links and links, links on links on links. Let's actually call this nav links. And we'll just flesh a few of these out real quick.\u003C/p>\u003Cp>Nav links, that'll be an array equals array. To, about, contact, no. That's we want none of that. Home, we got what? Categories.\u003C/p>\u003Cp>We'll come back to it. Let's just nuke that for now. Or we could just comment it out. Alright. As far as what we want inside the actual body of this here, we'll do this in the main section, we are going to fetch our job listings, right.\u003C/p>\u003Cp>So Nuxt has a couple of nice composables for fetching data. We're going to use that here. But the first thing I'm going to do, let's import the read items operation from my Directus SDK. Nope. Directus SDK.\u003C/p>\u003Cp>And then I'm going to use the async data call, the async data composable from Nuxt. Use async data, And this handles like caching and deduplication, and actually if this is server rendered it will automatically dedupe those calls and pass that data from the server to the client so that I don't have to fetch the data twice and hit that API. So we'll give this a, let's say this is listings index. We'll give it a key and then we'll actually fire this off. And we're going to actually await this data.\u003C/p>\u003Cp>Alright. Then within here, we're actually going to use our Directus client that we've set up. We're gonna access that through our Nuxt plug in and we are going to do this. So we'll say return Directus dot request. We'll do read items.\u003C/p>\u003Cp>We're gonna read from the what do we call it, actually? Call it the Job Listing Collection. So that's going to trip me up this whole time. And then we have a query that we could set up. We could run this where it says filter status equals published, but I'm handling that here within my access control inside Directus, so it doesn't really matter.\u003C/p>\u003Cp>So as long as this is published, like if I go in and let's just say I change this. Change this. We say we maybe set this to draft, and I hit save as copy. And I probably need a new slug for this. And I would definitely make slug unique here, so let's just do that quickly before I forget.\u003C/p>\u003Cp>We'll go into the data model, we'll go to listings, we'll make sure that slug has to be a unique value. Alright. So this will be listings and then I can I typically do something like this whenever I'm building just to quickly and easily make sure I'm getting all the data that I want back from my API? Alright. So we go to local host.\u003C/p>\u003Cp>We can see we've got this job board and I need to actually get this out of the header and into the main body here. Alright. So there's our application, our job listing, basically. Alright. That's great.\u003C/p>\u003Cp>We can see our tags, we can see the company. We can't really see any of the information about them, though. Right? And if we take a look at our postings here, let's see what we've got. We've got the company information, we've got the company name, etcetera.\u003C/p>\u003Cp>So how do we go and fetch that data with inside Directus? We'll just go in here. We'll do something like this where we have fields. I don't recommend doing it this way in production, just using these wildcards. But for something like this where I'm quickly prototyping, super handy for that.\u003C/p>\u003Cp>I could just get all of the root level fields, I can get all of the company level fields, I could get the category, category dot star. And then for our tags, I'm going to get tags dot tags underscore ID. I'm going to get that field. So if I do that now, you can see I get the extra information that we're looking for. So I've expanded my relationships for tags, for the company, for our category, basically everything I need here.\u003C/p>\u003Cp>Looks great. Looks great. Okay. And I'm getting both of those things here because I am using session authentication. I've got a cookie here that's storing my session token.\u003C/p>\u003Cp>So if I wanted to, I could go into incognito window and that should solve it where I'm only getting one of those items back. Alright. So I'm gonna pull this up. We work remotely over here on the side just so I can see that. And we'll start fleshing this thing out.\u003C/p>\u003Cp>So within our listings, we've got, let's do a div. Probably add, like, a h two here. This is our job listings. Get that font bold. Text XL.\u003C/p>\u003Cp>Maybe 2 XL. Alright. And maybe we give the whole main section a little bit of padding. I I think there's a U container Nuxt UI component that we'll just use for this. I'm using uidot nuxt.com, just a Nuxt UI library, just to have something to build with quickly.\u003C/p>\u003Cp>Alright. So we've got our job listings. Let's go through we're gonna do a card component for each one of those listings. So v 4 job in listings. The key, we can use the job dot ID.\u003C/p>\u003Cp>And here, inside the card itself, let's see what we got. Job title. Is this actually gonna be what we want? What's this gonna look like? Remote.\u003C/p>\u003Cp>It's not really anything that we we actually want here. This is gonna be company name, category dot name, Category dot title. And and this could be remedied by TypeScript if I had all these things typed probably. But as far as the tags, that's going to be the title of the tags as well. Cool.\u003C/p>\u003Cp>Alright. So how can we get this to look a little more like what we have? We'll go through and flesh this out a bit. Wrap this. We'll put it in a, it doesn't even need to be a grid.\u003C/p>\u003Cp>We could just do like a single grid. Give it some gap. 6. Alright. We got some space there.\u003C/p>\u003Cp>We'll give it a little breathing room. Cool. Alright. So we've got some job listings. We're going to want to give these a link.\u003C/p>\u003Cp>Where did where did that go? Oh, just disappeared. Need to learn my keyboard shortcuts, don't I? What are we what are we doing, Brian? That did it again.\u003C/p>\u003Cp>Alright. Maybe I'll just use the cut paste here. Wrap this in a Nuxt link component. We're gonna do this, the 2 for this will be something like this, where we have listing, plus job ID. Yeah.\u003C/p>\u003Cp>Potentially let's actually do it this way, just use a template literal. Job dot slug. Wrap that, don't forget the ending tick. Cool. And now we have a link that does not work because we don't have a route for that.\u003C/p>\u003Cp>But let's see if we could show the company information as well. Maybe we put that here. It would be within a div. We got the company name. Show Nuxt image source dot company logo.\u003C/p>\u003Cp>See if we can get that displaying. Okay. Now we'll shrink that a bit. Object Oh, hang on a moment. Just go here.\u003C/p>\u003Cp>Class, object contain, make it 24 high, 24 wide. Cool. Within this, we could do, like, a flex, add a little bit of gap. And, you know, if we wanted to, we could get fancy with it. But let's, how do we set this off a little bit?\u003C/p>\u003Cp>Oh, actually, the title for the company is over here on this side in it. Alright. So go in, wrap these in a div. That should give us like some type of formatting here. And if I really wanted to make it look like the one on WeWork remotely, and I don't, we'll just add a border for this, round it a little bit, get a little bit of padding, p 1.\u003C/p>\u003Cp>And maybe we make the border pretty. Make it violet. There we go. That looks poor, actually. I don't like it.\u003C/p>\u003Cp>Alright, we'll make it more subtle. There we go. So we got Directus, we got Change This as the name. We've got our job listings. How are we doing on time?\u003C/p>\u003Cp>Time wise, we're at about half an hour, right? Alright, so we've got some\u003C/p>\u003Cp>Speaker 1: job listings. We\u003C/p>\u003Cp>Speaker 0: can see a list of those. How would we do something like a filter for these? Or we'd probably actually want to list these by categories. Right? So we'd probably actually switch up our data fetching here into, we'd probably look for categories first.\u003C/p>\u003Cp>But instead of diving into that specifically right now, let's actually get a detail page up. So we've got a route for this index page. Let's go in and add a route for our listings. Great. And then we'll go in and add a new page.\u003C/p>\u003Cp>Let's call this the slug. View. So we do have a listing there. And maybe I just copy pretty much all of this. We'll actually, what do we wanna keep here?\u003C/p>\u003Cp>Let's just keep the card component. That's got our information. Maybe we keep the u container just so we get some base styling. And if I wanted to, I can move this header component out of this and into our actual app itself or in probably like the default layout would be a good one as well. Just post that there.\u003C/p>\u003Cp>So now that will show on every single page. Alright. So as far as our job listing, we're going to fetch that data here. I'm still going to use the read items call, because we're fetching by the slug and not the actual ID. So what we're going to do inside Knox would be something like this, where we use the route composable, or use route composable to get the route.\u003C/p>\u003Cp>And then we're going to set\u003C/p>\u003Cp>Speaker 1: up a filter for\u003C/p>\u003Cp>Speaker 0: our slug, and we want that to be equal to route. Params.slug. So this should give us our listing. And then what we're going to do here is at the end of this, using the use async data and the I think it's use fetch composable inside Nuxt have an option set where I can transform this data when I return it. So this is going to give me an array and I only want the first item of the array.\u003C/p>\u003Cp>So even though this only matches one thing because the slug is unique, it still returns an array because of the read items SDK, or the read items method of the SDK. So we're just gonna transform that. And then here inside our card, we don't really need that anymore. Right? So this gives us what we want.\u003C/p>\u003Cp>Cannot read properties of undefined of slug. Why do we not have this? Right? Here's our listings. There's our slug.\u003C/p>\u003Cp>Route.params.slug. Directus.requestreaditemsjoblisting. Cannot read properties of undefined of slug. Let's just comment this out, see if the page actually loads. Yes.\u003C/p>\u003Cp>It does load. Let's figure out why we're not seeing that. So, the Vue dev tools is is really a great tool if you're developing with Vue. We will look for the slug component. Let's see what we got.\u003C/p>\u003Cp>We got listings. Oh, that's why. That would make sense. Right? Sometimes it's the typos, especially when you're building quickly.\u003C/p>\u003Cp>They get you into trouble. So here, let's just remove the card. I don't even need this Nuxt link component anymore. Okay. And within our layout, let's make it let's get some navigation so we can actually move backwards here.\u003C/p>\u003Cp>Make this a Nuxt link. Nuxt link. Change it to 2, do slash. Cool. So now I could go should be able to go back and forth.\u003C/p>\u003Cp>There we go. Go here. Go there. Amidst options next to\u003C/p>\u003Cp>Speaker 1: header slot.\u003C/p>\u003Cp>Speaker 0: What am I doing wrong there? Alright. Anyway, so this is gonna be our job. And now we have got our job listing. Cannot read properties of type.\u003C/p>\u003Cp>Alright. Let's go back into Directus. And is this because of our our access control? We've got it. Let's set it up so we can read the files as well.\u003C/p>\u003Cp>Does that give us what we want? Okay. So at least now I've got some type of card information here. How do we want to display the actual content for this? If we go into one of these, we've got, the job title, the logo here is on the right as well as the company.\u003C/p>\u003Cp>So we can go in and flesh this out. You know, we could spend a ton of time on design here. I'd rather focus more on the important bits. So let's just do a main component. This is where we'll stuff the content.\u003C/p>\u003Cp>We could do v dot vhtmlordiv.vhmml, v dash html. That'll be our job dot content. Okay. That should show. Cool.\u003C/p>\u003Cp>This is not really nicely formatted. Right? So, using Tailwind, they have this really nice pros class we could use to get that formatting. Looks pretty great. Maybe we add some padding to this container.\u003C/p>\u003Cp>I don't even need that. Strip this away or not some padding. Give it some margin. This looks a little odd here at the top. Let's remove that.\u003C/p>\u003Cp>We've got Directus is the company. And this could be another NuxLink. This could be, what, to the company?\u003C/p>\u003Cp>Speaker 1: 2/company/job.company.slug.\u003C/p>\u003Cp>Speaker 0: That's what that would look like. And inside this div, maybe we just add some spacing. Okay. Alright. So now we could potentially navigate to the company, but there is no page for that.\u003C/p>\u003Cp>But here we've got our actual job posting. How do we we want to use like a button for you button. Is it the apply URL? I think I've got application URL here. Apply for\u003C/p>\u003Cp>Speaker 1: this position.\u003C/p>\u003Cp>Speaker 0: Great. What's that gonna do? Job application URL, is that what I set this up as? Let's take a look at our data model. Application URL, job application URL, that should be correct.\u003C/p>\u003Cp>And we want this to be let's make this a block class. Block. And maybe we drop that below the actual name. We don't have a description for this. It's great.\u003C/p>\u003Cp>Maybe we do this. Just set off this metadata just a little bit. Okay. We hit apply for\u003C/p>\u003Cp>Speaker 1: this position. And this\u003C/p>\u003Cp>Speaker 0: should open that in a oh, it's because I've got HRF. Let's try this. Apply for this position. That should give me this application page. Great.\u003C/p>\u003Cp>What is this going to be? In line block? There we go. Okay. So now we've got our page, we've got our company information, we've got our job description.\u003C/p>\u003Cp>We can drop another button at the bottom of this content. Looking nice. Alright. We are cruising on, like, 20 minutes worth of time. Right?\u003C/p>\u003Cp>So how can we actually create a post? Right? We've got our post. Let's go back and we'll do a new route. We'll just call it listings dot new.\u003C/p>\u003Cp>I'm just going to copy the same setup that we've got. Just moving really really quickly. Just drop everything out of here. We're gonna keep Directus. In this case we're gonna use create items.\u003C/p>\u003Cp>Create item. Great. And on the front end, if I wanted to be able to create a listing, we're going to need to add some navigation for this. So we'll just, let's actually stick this inside our header. Right.\u003C/p>\u003Cp>Where are you? That's in our default layout. So over here on the far right, we can actually flex these items, justify between. Oh, sorry. I already had the flex.\u003C/p>\u003Cp>Great. And we'll do listings dot new, post a job. Great. That takes us to the job new page. And on the new\u003C/p>\u003Cp>Speaker 1: page, why do we not okay.\u003C/p>\u003Cp>Speaker 0: Okay. So now we are going to post a job. We've got let's take our same heading here. Post this in here. We've got a create a new job post.\u003C/p>\u003Cp>Okay. Alright, so now we can start fleshing out our actual form that we want, right? So let's just do form. This could be reactive since we've got some properties here. What all do we have inside that listing?\u003C/p>\u003Cp>Right. We've got a title. You know, maybe we don't want them to actually choose the slug. We've got a category. That should just be an ID.\u003C/p>\u003Cp>We got some tags. This is gonna be an array. We got some content. That's gonna be a string. We got an application URL, and we've got a company, which is, it's actually, should be a string, just an ID, but we'll have to actually fetch that information.\u003C/p>\u003Cp>And then at the end of this, we are going to have a function to submit the form. Alright. Submit listing. And what's that function gonna look like? Oh, let's do a try catch.\u003C/p>\u003Cp>This is gonna be a constant response equals await, direct us dot request. Create item. Yes. This looks good. Form dot title.\u003C/p>\u003Cp>Okay. Alright. GitHub Copilot. I got you. And then what are we gonna do?\u003C/p>\u003Cp>Console log response. And we could send them to like a thank you page or something. Maybe we are tracking like a success state here. And we set success value to true. If there's an error, we're going to log that error.\u003C/p>\u003Cp>Great. Cool. Alright. So we got submit listing. Now we need to actually build a form.\u003C/p>\u003Cp>Right? Inside this Nuxt UI library, I think there is a, let's just dive into it, form component. You can even add validation with this using, like, Zod or something like that. But, we've got our form wrapped in a form. There's a form tag.\u003C/p>\u003Cp>We've got at submit. Let's do dot prevent. Submit listing. That's going to be our form handler. And then inside the form, each one of those gets wrapped in a form group, where we got a label equals title.\u003C/p>\u003Cp>Placeholder equals, amazing Directus developer. That'll be our placeholder\u003C/p>\u003Cp>Speaker 1: for this.\u003C/p>\u003Cp>Speaker 0: So we that's probably actually on the actual input itself. Alright. And then we have a u input form dot title. We have a placeholder. Let's see what we're getting now.\u003C/p>\u003Cp>Cool. Amazing Directus developer. Maybe our form isn't so wide. 3\u003C/p>\u003Cp>Speaker 1: x l.\u003C/p>\u003Cp>Speaker 0: Did that bring it in? It did not. Math would okay. Still a little long, but no worries. Okay.\u003C/p>\u003Cp>So we've got our title. We want to go through and add the rest of our form. We got a category. Let's see what GitHub Copilot can do here. Yeah.\u003C/p>\u003Cp>It's not giving us a lot of help here. But we can drop some of these inputs. Maybe we add some spacing between our form components here. So we got tags. We got content.\u003C/p>\u003Cp>We've got an application URL. Let's make sure we get that. Application URL. And, alright. So for this, like, the company information, the category, etcetera, This could be a list of companies.\u003C/p>\u003Cp>Right? So what do we have inside Nuxt UI for something like this? We've got a drop down. Is that what we're looking for? Does this have a search?\u003C/p>\u003Cp>No. That's gonna be input,\u003C/p>\u003Cp>Speaker 1: input menu. I'm assuming\u003C/p>\u003Cp>Speaker 0: this could be an input menu based on the preset. This is searchable. Search attributes, control the query, Async search. Okay. Here we go.\u003C/p>\u003Cp>So we got a async search function. We can search for a company inside Directus with this. This will be an async function. Let's call it search categories or search companies, maybe. Let's do the company first just to, well, actually, let's do categories.\u003C/p>\u003Cp>We have them fill out the the company information. Constant, what our data is gonna be await, direct us dot request. That's gonna be read items. We're probably gonna need, what, like, a search query as well. That'll be passed here.\u003C/p>\u003Cp>This will be our string. Loading value, read items dot categories or categories is gonna be here. Inside that request, we're gonna need a filter. Actually, let's use a search for that. And we'll just do search query.\u003C/p>\u003Cp>Now the search param here is not optimized like the filter params are, but let's see what we've got here. You form group, you input menu. Oh, wrong one. We want this to be our let's drop this in our category. You input menu, search equals search categories.\u003C/p>\u003Cp>This is gonna be form dot category. Search for a category. And we can set a loading state for this as well. Alright. Let's see what we've got here.\u003C/p>\u003Cp>Search for a category. In line properties, not length. If I do this, is it actually searching for a category? Programming? I don't see it.\u003C/p>\u003Cp>Search search categories, loading, form dot category, placeholder, option name, attribute is a title. Title. Alright. Let's see what else we got here.\u003C/p>\u003Cp>Speaker 1: I'm missing\u003C/p>\u003Cp>Speaker 0: something here. Oh, forgot to return something from that actual function. Gotta return the data, Brian. Makes sense. Alright.\u003C/p>\u003Cp>See if we got anything else now. Is this actually working? I don't see it calling any of the data, though. Direct is at request, read items, categories. Did I, oh, that's why.\u003C/p>\u003Cp>Got to import read items as well. Alright, so we refresh. Programming. Okay. Now we see sales and marketing.\u003C/p>\u003Cp>And inside my actual listing, right, where's the form? So we got our slug. This is actually new. Alright. So we got our form.\u003C/p>\u003Cp>We got the category. We need the ID of that category. Selecting the actual object. Option Attribute Title. Category dot title?\u003C/p>\u003Cp>Is that going to give us what we want? Oh, no. Option attribute by ID. Why is it not storing when it selects the value what do we actually want here? V model async search.\u003C/p>\u003Cp>Use the debounce. You know, we could add a debounce to it. Let's actually deal with that in a moment. We need to actually submit this form first. Alright.\u003C/p>\u003Cp>For the input menu, what else do we have? Okay. I see like a select menu here that is probably very similar to this. Does this one support multiple objects? It does.\u003C/p>\u003Cp>So I can select multiple tags for this. This is probably gonna want be what we're looking for there. We can actually probably genericize this as well. We have a string. We have a collection, which is a string.\u003C/p>\u003Cp>This is gonna be our search. This is gonna be a collection. And in this case, our search would be search categories. What's that gonna be? The categories?\u003C/p>\u003Cp>Actually, categories. So it'll just be search. Does that work? Is that gonna give us what we need? For some reason, that doesn't work.\u003C/p>\u003Cp>Undefined. Surf search f n. Do I have, like, another search variable\u003C/p>\u003Cp>Speaker 1: or something? String. Q.\u003C/p>\u003Cp>Speaker 0: Alright. Does that get us what we're looking for? Let's try it again. There we go. Okay.\u003C/p>\u003Cp>Alright. So now we're getting that. Let's copy in our use select menu. Does this have the same async search capabilities? It does.\u003C/p>\u003Cp>Our use select menu, this is going to be multiple, so we'll get our tags. The model equals form dot tags. Our search function here is going to look like this. So we get our search functions. This is going to be tags.\u003C/p>\u003Cp>Search for tags. We've got the option name. It's gonna be a title. That could be multiple. It could be by ID.\u003C/p>\u003Cp>Great. And now we can see, like, full time, part time. We got some tags selected. Great. Alright.\u003C/p>\u003Cp>Let's add some buttons, just a single button to our form group. We'll make it type equals submit. And what are we gonna do here? This will just be submit listing. Okay.\u003C/p>\u003Cp>We've got a job post. Cool. For the sake of this, we've got, like, 7 minutes running on the clock. I am going to allow people to submit a new listing here publicly. I would probably make them log in or create an account first, but, for the sake of just getting this done, let's try it this way.\u003C/p>\u003Cp>And then here, form dot title dot tags. Let's just see what we're getting first. Here's a new post. When I search for my programming category, we'll apply a couple of tags here. Here's the amazing content for our job post, and we just got a new link.\u003C/p>\u003Cp>So httpdirectus.i0. If we look at our form component, let me close this. So open this back up. New is the name of this actual component. Why don't I see it?\u003C/p>\u003Cp>New. There it is. We can see the state of our form. Our tags are an array of objects. Our category is an array of objects.\u003C/p>\u003Cp>Here, I'm just gonna do this where we do category form dot category dot ID. I think that should work here. For our tags, probably going to have to do some type of mapping dot map tag dot tag dot id. Okay. Oh, no.\u003C/p>\u003Cp>I lost my amazing post. We are coming up very quickly on the end. We need an amazing Directus developer and a 100 apps, a 100 hours host. Probably after I fail at this one, we'll do design, full time, SEO, select 5 tags. Here's some content for this post.\u003C/p>\u003Cp>We'll do the direct us URL, and let's see what happens. Alright. We'll do network request. Post. I don't see anything happening.\u003C/p>\u003Cp>Submit listing, submit listing, type equals submit within the form, Submit listing at submit dot prevent listing. Does this need do I need, like, a click handler on this? I shouldn't. Let's try it anyway. Submit.\u003C/p>\u003Cp>Why are we not making a single call here? Submit this. Why are we not submitting? Prevent default. At submit dot prevent.\u003C/p>\u003Cp>And maybe it automatically does that for you. Man. I hate to cut this down to the wire. We are gonna cut it down to the wire. Test.\u003C/p>\u003Cp>Test. Array of errors. We got what? Internal server error. What do we actually send to the server?\u003C/p>\u003Cp>Here's our payload tags 12, application URL, category is 2, company is test tag. What kind of response did we get? Why is this an application error? Job listing, submit listing, direct us to request, create item, category. Man.\u003C/p>\u003Cp>This is frustrating. Form.application.url. Click submit listing. Why isn't this going through? Maybe we unselect these things.\u003C/p>\u003Cp>Can I unselect 1? Submit listing. Errors. Errors. Errors.\u003C/p>\u003Cp>Internal server error. Oh, man. There's something that I'm not picking up on. Create item, job listing, title category, form dot ID. What if we just drop these 2?\u003C/p>\u003Cp>Can we actually get this to submit something? Content. Test. Job listing. We've got the public ability to do the job listing.\u003C/p>\u003Cp>Job listing tags. Is that what it is maybe? Alright. We're just throwing the Hail Mary pass here. You can edit anything.\u003C/p>\u003Cp>You can edit anything. You can edit anything. And now we're still getting errors from the direct instance. Internal server error. Is it like a cores issue?\u003C/p>\u003Cp>You know, if I log out, if I do this in an incognito window, test, test, test. Still getting it right. Items dot job listing response redirected false errors. An unexpected error occurred. What do I have wrong inside this particular setup?\u003C/p>\u003Cp>Title, status. Is it the slug that I made unique? Slug has to be unique. Is that what it is? Test.\u003C/p>\u003Cp>I'm going to kick myself after we run out of time on this one. Item, job, listing, post. What what is going on? Is it a a particular, like, a chorus problem? I thought I had chorus set up.\u003C/p>\u003Cp>Chorus enabled. Chorus origin. This is a oh my gosh, dude. If it is a chorus problem, we're gonna kick myself if it's a course issue. Docker Compose.\u003C/p>\u003Cp>Let's see what we've got here. I see some logs. Error, insert into job listing. Title created values. Invalid input syntax for ID.\u003C/p>\u003Cp>Why is it creating an ID? Should not be creating\u003C/p>\u003Cp>Speaker 1: an ID.\u003C/p>\u003Cp>Speaker 0: I don't know what's going on there. Alright. Let's try this again. Test. Test.\u003C/p>\u003Cp>Test. Error. Error. Error. Database.\u003C/p>\u003Cp>Into job listing. Invalid syntax input for UUID. Like, UUID should be getting automatically populated though. Application URL. What what am I not sending?\u003C/p>\u003Cp>Oh, it's the company. It's the company field. Oh my gosh. The company field is killing us there. Okay.\u003C/p>\u003Cp>Alright.\u003C/p>\u003Cp>Speaker 1: 4 seconds left. Submit. Job Listing.\u003C/p>\u003Cp>Speaker 0: Nothing. Still errored out. Boom. Time exploded on us. Just out of curiosity, it was the company field that was messing things up.\u003C/p>\u003Cp>Now, we're still getting, now I'm getting like course errors or something. Or, no, does it show it posted? Did we actually get a job listing in here? Yeah, we did. Of course.\u003C/p>\u003Cp>After time, the company field getting us, I should have like, actually ran that down. Did not. That's the way these things go, man. It's always hairy trying to develop against the clock. Alright.\u003C/p>\u003Cp>Let's just test this, test with categories. I'm curious if this will actually work now because we have the, now I'm getting cores errors. Alright. This is just me humoring myself at this point. PMPM dev.\u003C/p>\u003Cp>Let's restart the Docker container. Oh, no. We don't need to do dev. Okay. So we restart this Docker container, fire back up this Nuxt application, see if this is actually going to do something.\u003C/p>\u003Cp>Just again, me humoring myself here. Correctus is online. Nuxt application is online. Is this actually going to submit? Local host 3,000, post a job, test with categories, can't even spell.\u003C/p>\u003Cp>Programming, apply some tags. This is a great job. Here's the test. We hit submit. I probably need to add some submission there.\u003C/p>\u003Cp>You don't have permission to access this. Why don't I have permission to access this? I've got public controls. Tags, content is great. Oh, it's because, the Junction collection.\u003C/p>\u003Cp>Oh, boy. Yeah. So, tags is actually a should be something like this where I have tags what's this gonna be? Tags underscore ID equals tags ID. Alright.\u003C/p>\u003Cp>So in that case, now, new is this\u003C/p>\u003Cp>Speaker 1: actually going to be what we want it to be?\u003C/p>\u003Cp>Speaker 0: Full time ID. So when it gets submitted, I'm hoping that should be what we want. Tag underscore ID. Tags underscore\u003C/p>\u003Cp>Speaker 1: ID. No. Save without formatting.\u003C/p>\u003Cp>Speaker 0: What are we doing here? Sometimes you can't win them all. Sometimes you just can't win. Test. Test.\u003C/p>\u003Cp>Does that actually submit? It does submit with the category. So there we go. Roundabout way of getting to this. We still didn't get exactly where we wanted, but, you know, what did we actually get to here?\u003C/p>\u003Cp>I blew this one up. So we managed to show a list of jobs. We let users kinda post a job for free. Were these sorted by category? We didn't even do that.\u003C/p>\u003Cp>Subscribe to job posts, we didn't even get there. Man, alright, that's the way it rolls. Thanks for joining me on this episode of 100 Apps, 100 Hours. I'll catch\u003C/p>\u003Cp>Speaker 1: you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 Apps, 100 Hours. I'm your host Brian Gillespie and on the show we build some of your favorite apps or some of your suggestions or just random stuff that I've come across on the Internet in 1 hour or less or get publicly shamed trying when we fail. And I fail often. That's just the rub of this format. The rules are there are 60 minutes to plan and build the application, no more, no less. Sweating just thinking about it. But rule number 2 is use whatever you have at your disposal. AI, GitHub Copilot, tailwind CSS, tailwind UI, any pre made components, whatever we can to get the job done in 1 hour. Today, we are going to be building a job board. So the giant elephant in the room in this space is obviously Indeed. They are one of the largest job boards. Yeah. It's been a while since I've been on their site. The experience is not great, honestly. Maybe everybody's working on a a mobile or something, but, I I think the the job board that we're gonna model after today is we work remotely. We are an all remote team here at Directus, so this speaks to my heart. I've been working remotely for, the better part of a decade now. So, we are going to model after this. Let's dive in and get started, shall we? Alright. So we'll put 60 minutes on the clock. Let's just talk through the functionality that we're looking for here. And As I pull up the WeWork Remotely job board, what's our functionality? Right? We want to, obviously show a list of jobs. In this case they are sorted by category. We want to let someone let users post a job for a fee. That's nice. And, you know, this one's got subscribers as well, which seems like something that is relatively simple. There's a lot of moving pieces to that. So let's add a question mark and we'll do subscribe to job posts. Now I think this one does not allow you to apply within the platform. I think it just, like, sends you off to a third party thing. So that's the way we'll play this, you know, if you wanted to build like an application processing system, you could totally do that with Directus. That's a lot of ground to cover in 60 minutes though. So let's take a look at this, let's flesh out our actual data model, right? That's part of the planning process, so I'm just going to drag some boxes over here inside Figma. What are we going to have? We're going to have job posts or we could call it job listings. That's great. Within that, we're gonna have a few different things. Alright. What do we got? We got some tags, it looks like. So we got a category. We got a title for that post. We've got some content, we've got a link, we've got a like, a company. I guess if you're going to have a company, that would probably be here. That'd be a separate table. Right? There's a company. Company has a name, a website. Name. Website. What? Logo. Great. K. What else are we gonna have? If we make it 2 subscribers, we'll worry about that when we come across it, but let's dive in. Right? This seems pretty good. We'll just draw some arrows to make it look fancy. Right? A company has multiple job listings. One listing has one company. Alright. Great. Let's talk about what we're working with. So as far as applications, I've got a direct assistance spun up, it is blank. I've got a bare bones Nuxt app that has some login and register capability. Looks like I'm already logged in though, so all good there. And as far as the Directus communication, I've already got a Nuxt plug in defined just to talk to Directus a little bit easier, make it a little less boring on this specific episode. Alright. So let's start by I'm going to pull this up side by side with remote. Oh, no. That's not what we want. I could spend the whole time just messing with the Arc Browser. Alright. So we're talking about job listings. We're going to go ahead and add that as a new collection. And I think I can zoom in a little bit to make that more visible. And then we'll look at the actual listings that we have. Right? One of the other things that I do often is, I'm not sure how this site is put together, but, you know, on sites that are server side rendered, you can go into the actual code and especially if it's, like, a Next application or, I'm not sure if Nuxt does this or not, but you can actually see the data that's posted. I'm not sure if that's the case here. I see Jquery. This is probably server rendered from an actual server. No big deal. But, just one way you can kind of see what other folks are doing on the website, always inspect the code. I live in the JavaScript developer tools console. Alright, so we're going to have a job listing. We're going to have a pretty slug for all of these. So let's see what we're going to do, we're going to have a draft and publish listings, probably got a date created, did the user created, the date updated, etcetera. And let's go through and model out our listing, right. So we've got a title for the listing, we're going to have that. We've got to have a slug for the listing. So we'll just do a slug for the listing. We'll come back to that one in a moment. What else are we going to add? We're going to have some content. I see there's tags here. I don't know exactly how they're using the tags, but it looks like the the tags get their own page. So in that case, it makes me think we need, like, a relationship there. Let's look at that. We'll do a mini to mini relationship because one post could have many tags and there could be many tags to other posts. Alright, So we got tags as a key. I'm going to enter tags as a related collection here, right? And if I open up the advanced field mode inside Directus and I scroll down to the bottom, you'll see that, it's actually going to create a new table for me. So that tags table that I don't have, it's going to create it for me, which is kind of nice, right? And I can add this to the tags, I can show the job listings on the tags that have that specific tag. And maybe we can sort these as well. Cool. Alright. So we'll hit save. We've got tags. We also should have a tags collection now, which is nice. We have a junction table for those as well. Then we're gonna need some content. Right? We need the actual, content for this. That's gonna be the WYSIWYG editor. Is that great? Cool. The type will be text, that's what it's going to be saved as in the database. I can always adjust my toolbar if I need to. And, what else do we need really? Apply for this position, right? This is going to be a URL. So apply URL, Application URL. That's what we'll roll with. We're probably going to need a category for this as well. So if I go back to all the jobs, we can see we've got sales and marketing. If I zoom out a little bit, we can see all the different categories up here at the top, Programming, Design, etcetera. So we're going to create a new relationship inside Directus. Let's call it category. And we will again create a new collection here. So if, even if this doesn't exist, Directus is going to create this for me, which is, you know, super. It's a really easy, super nice way to do our data modeling. We can add the different job listings to the categories if we want to. In this case, maybe I don't need to show that. But we will, we don't have anything to adjust as far as the interface because this is not created yet, but we'll just go through the process. So we've got a category for this, We've got tags. And if I go back to my data, I'll see categories here now. So we've got categories, we've got tags. Great. What else are we missing on this? Is this a featured post or not? I'm assuming that's part of the checkout process. So, you know, we could add a Boolean tag for that is featured. I always like when I'm doing my keys, especially if they're Boolean prefix with the is. There's just a nice little standard naming convention. And on my interface, maybe the label is featured post. Great. Okay. Alright. So this looks pretty good, I want to say. Alright. We'll make some of these things half width. Maybe we make the title full width. We got the status. Actually, let's do this. We got status, we got title. Let's put slug down below title. And don't sleep on the Directus marketplace. Right? One of the extensions that I've been using a lot here lately is the WP slug interface extension. If you're used to WordPress, you got, like, this nice little slug interface there. We're just gonna install this, give it a quick refresh. Then I go back to my data model, I go to my slug, go to the interface, and I can switch this up to my slug. I get a template for this, so we're going to populate the title. I could potentially prefix this or, you know, auto generate it as well. So I'm just going to do On Create because after a thing is created, it's been published, changing the slug results in a different URL on your front end, could result in some nasty 404 errors. We don't want that. 40 fours get out of here. Alright. So we got a job listing category. We've got did I do job listing instead of job listings? I did. Man. Alright. How can we fix that? We're not going to bother with it. I could dig into the SQL and adjust that if I needed to, but we're not going to worry too much with it right now. Typically, I keep my table names plural. Just a personal preference. Alright. So we've got a category. Let's go ahead and flesh out these categories. We need a title for the category. We probably need a slug for the category as well. So I can go back into my job listings, and if I want, I can duplicate this field to that collection. So we'll just change this to slug. We'll go to categories. Great. And in that case, do we actually want the tags to have their own page as well? The tag will have a title and the tag will probably have a slug as well. So we'll just copy that across. Slug. Change the collection to tags. And away we go. Right? The last thing that we talked about is a company. So we want to be able to select a company for our post and you know potentially a company could post 3 or 4 different times. Maybe they're hiring for a lot of different positions. So let's create a new table. We're going to call it companies. We'll do a generated UUID. Do we need all this information? Probably not. Alright. So we'll give the company name. And whatever I choose to name this key is totally up to me. And then we've got the website. That's just going to be a string. And then we're going to have a logo for that. Right? Logo. Alright. Cool. So now we've got it's just a basic data model set up. Let's go in and I'm just going to, like, just rip some of this content. We work remotely. Sorry for just sealing information off your job board here. Let's look at our different categories. So we'll just mop up a couple of these categories really quickly. We've got what? Programming? Oh, gosh, I can't spell. Programming, save. We've got Design, save. What else do we have? Sales and marketing. Sales and marketing. And what happens if we do this? Sales and? Oh, that's kind of a nice effect. Sales and marketing. Maybe we change this to just be sales dash marketing. Customer support. Customer support. Great. Okay. And this is all remote, so, you know, what are we gonna call this thing? Maybe we'll call it it, Dan's Remote Job Board after some of our own teammates here at Directus. Alright. So we've got some categories. Let's take a look at the tags within here. We've got full time. We've got, part time will be a tag. Right? Part time. What? SEO? Link building, WordPress, technical writing. What do we have on the development side? So we got programming. We got full stack, front end. What kind of tags do we have here? Europe only, etcetera. Hang on just a moment. I got an important call coming in. Be right Alright. So now we're back. We've got 46 minutes on the clock. Let's dive in further. We've got some tags. We've got like Europe only, etcetera. Great. Okay. So, those are a few tags. We've got some categories. We've got, let's see if there's any companies that we recognize on here for full stack development. I don't really recognize any of these. Right? Let's just create a new company here. We'll call this Directus HTTP Directus dot io. And we'll add the logo for this. Right? See what I got handy. Directus logo. We'll get this a couple of different ways. Logotype. That looks good. That's what we will roll with. We got the logotype. Maybe we add another company, but for now let's just add our first listing. Right. I'm just going to steal this one, full stack. Alright. We are going to make sure this is published. Is this a featured post? Looks like it is. Alright. We'll go in. And I could just copy this wholesale. And I'm thinking it should retain some of the content, some of the settings. Yeah. Okay. So it should retain the formatting for it. And then we're going to have a link for the application. And the last thing that we need to do is pick a category. And we probably need to add a company for this as well. And I I joked, it's not the last thing we need to do. We're going to add a few more. This is going to be a Europe only job. Be a $100,000 or more USD. That's that's kind of a nice tag. Let's do React as a tag. Oh, sorry. Creating an item in a tag. React. Great. Alright. Is this a featured post? Yes, it is. Let's save that post and then we'll go in and add a relationship to that company. So that'll be a many to one relationship inside Directus. We're gonna call this company because there's a single company, and then we have companies. And, you know, we could go in and add the company name here so that it displays nicely. We probably want to do the same thing for our categories, like give them a title, make sure those show on the interface in the display template. Same thing for tags. For our list view here, we're going to do the tags ID dot title and we'll do the same thing here. Great. Okay. So now if I open up this job, we can see our different tags for this. We've got our category. We've got the company that this is created for. Sweet. Alright. So now we've got a job listing. We've got some categories, we've got some tags, we've got some companies. Does a company going to have a slug? You know, potentially. Do they have a listing here? They do. So probably need a slug for the company as well. Slug. The template is going to be the name of the company. Great. Alright. So we've got our data model for this, right? The next thing that I'm going to do is just go through our access control and create this. So there are 2 roles that are out of the box inside Directus. I have public, I have administrator. I'm going to go through and, just basically set read access to all these. And then I can go back in and adjust this if I need to as well. So we're going to adjust the item permissions just a little bit to where we can only see published posts. So I'll go through listing tags, categories. I don't know the categories. Do we do publish for categories? We did not. Tags, etcetera. Yeah. So the rest of those are fine as is. Right? People can see all the companies, all the tags, all the categories. But if it's not a published listing, they can't see that information. Alright. So great. Let's go in and now, is it time to actually start fleshing something out inside our Nuxt application? Let's let's take a look at that. Right? So we've got here on the how do I separate these 2? Alright. Cool. So, Nuxt application on our pages directory, we've got an index page. This is probably going to be just our listing of posts. Right? So we can start fleshing this out a little bit. Let's remove this centering. That should move it way up here to the top. We'll just add a header for this. And what are we going to have in the header? We're going to flex. We are going to do Dan's remote job board. That's the name of this thing. We'll make sure it's like a mono font for Dan. I know how Dan likes this stuff. Okay. Let's give this header a little padding. PX6 PY. Alright. And we can even make this like sticky at the top if we want to. Sticky, top 0. Give it a background white. BG opacity. We do that backdrop blur effect. One of the nice things about using tailwind, backdrop blur, BG opacity. I could actually just adjust it this way. Let's do dash or slash 50. And cool. Alright. So now we've got a header. We might have some links inside our header that we'll cover in just a moment. Let's do a div to ul. Links. Do we actually need these on a list? Probably not. Let's do what? Nuxt link v 4, links and links, links on links on links. Let's actually call this nav links. And we'll just flesh a few of these out real quick. Nav links, that'll be an array equals array. To, about, contact, no. That's we want none of that. Home, we got what? Categories. We'll come back to it. Let's just nuke that for now. Or we could just comment it out. Alright. As far as what we want inside the actual body of this here, we'll do this in the main section, we are going to fetch our job listings, right. So Nuxt has a couple of nice composables for fetching data. We're going to use that here. But the first thing I'm going to do, let's import the read items operation from my Directus SDK. Nope. Directus SDK. And then I'm going to use the async data call, the async data composable from Nuxt. Use async data, And this handles like caching and deduplication, and actually if this is server rendered it will automatically dedupe those calls and pass that data from the server to the client so that I don't have to fetch the data twice and hit that API. So we'll give this a, let's say this is listings index. We'll give it a key and then we'll actually fire this off. And we're going to actually await this data. Alright. Then within here, we're actually going to use our Directus client that we've set up. We're gonna access that through our Nuxt plug in and we are going to do this. So we'll say return Directus dot request. We'll do read items. We're gonna read from the what do we call it, actually? Call it the Job Listing Collection. So that's going to trip me up this whole time. And then we have a query that we could set up. We could run this where it says filter status equals published, but I'm handling that here within my access control inside Directus, so it doesn't really matter. So as long as this is published, like if I go in and let's just say I change this. Change this. We say we maybe set this to draft, and I hit save as copy. And I probably need a new slug for this. And I would definitely make slug unique here, so let's just do that quickly before I forget. We'll go into the data model, we'll go to listings, we'll make sure that slug has to be a unique value. Alright. So this will be listings and then I can I typically do something like this whenever I'm building just to quickly and easily make sure I'm getting all the data that I want back from my API? Alright. So we go to local host. We can see we've got this job board and I need to actually get this out of the header and into the main body here. Alright. So there's our application, our job listing, basically. Alright. That's great. We can see our tags, we can see the company. We can't really see any of the information about them, though. Right? And if we take a look at our postings here, let's see what we've got. We've got the company information, we've got the company name, etcetera. So how do we go and fetch that data with inside Directus? We'll just go in here. We'll do something like this where we have fields. I don't recommend doing it this way in production, just using these wildcards. But for something like this where I'm quickly prototyping, super handy for that. I could just get all of the root level fields, I can get all of the company level fields, I could get the category, category dot star. And then for our tags, I'm going to get tags dot tags underscore ID. I'm going to get that field. So if I do that now, you can see I get the extra information that we're looking for. So I've expanded my relationships for tags, for the company, for our category, basically everything I need here. Looks great. Looks great. Okay. And I'm getting both of those things here because I am using session authentication. I've got a cookie here that's storing my session token. So if I wanted to, I could go into incognito window and that should solve it where I'm only getting one of those items back. Alright. So I'm gonna pull this up. We work remotely over here on the side just so I can see that. And we'll start fleshing this thing out. So within our listings, we've got, let's do a div. Probably add, like, a h two here. This is our job listings. Get that font bold. Text XL. Maybe 2 XL. Alright. And maybe we give the whole main section a little bit of padding. I I think there's a U container Nuxt UI component that we'll just use for this. I'm using uidot nuxt.com, just a Nuxt UI library, just to have something to build with quickly. Alright. So we've got our job listings. Let's go through we're gonna do a card component for each one of those listings. So v 4 job in listings. The key, we can use the job dot ID. And here, inside the card itself, let's see what we got. Job title. Is this actually gonna be what we want? What's this gonna look like? Remote. It's not really anything that we we actually want here. This is gonna be company name, category dot name, Category dot title. And and this could be remedied by TypeScript if I had all these things typed probably. But as far as the tags, that's going to be the title of the tags as well. Cool. Alright. So how can we get this to look a little more like what we have? We'll go through and flesh this out a bit. Wrap this. We'll put it in a, it doesn't even need to be a grid. We could just do like a single grid. Give it some gap. 6. Alright. We got some space there. We'll give it a little breathing room. Cool. Alright. So we've got some job listings. We're going to want to give these a link. Where did where did that go? Oh, just disappeared. Need to learn my keyboard shortcuts, don't I? What are we what are we doing, Brian? That did it again. Alright. Maybe I'll just use the cut paste here. Wrap this in a Nuxt link component. We're gonna do this, the 2 for this will be something like this, where we have listing, plus job ID. Yeah. Potentially let's actually do it this way, just use a template literal. Job dot slug. Wrap that, don't forget the ending tick. Cool. And now we have a link that does not work because we don't have a route for that. But let's see if we could show the company information as well. Maybe we put that here. It would be within a div. We got the company name. Show Nuxt image source dot company logo. See if we can get that displaying. Okay. Now we'll shrink that a bit. Object Oh, hang on a moment. Just go here. Class, object contain, make it 24 high, 24 wide. Cool. Within this, we could do, like, a flex, add a little bit of gap. And, you know, if we wanted to, we could get fancy with it. But let's, how do we set this off a little bit? Oh, actually, the title for the company is over here on this side in it. Alright. So go in, wrap these in a div. That should give us like some type of formatting here. And if I really wanted to make it look like the one on WeWork remotely, and I don't, we'll just add a border for this, round it a little bit, get a little bit of padding, p 1. And maybe we make the border pretty. Make it violet. There we go. That looks poor, actually. I don't like it. Alright, we'll make it more subtle. There we go. So we got Directus, we got Change This as the name. We've got our job listings. How are we doing on time? Time wise, we're at about half an hour, right? Alright, so we've got some job listings. We can see a list of those. How would we do something like a filter for these? Or we'd probably actually want to list these by categories. Right? So we'd probably actually switch up our data fetching here into, we'd probably look for categories first. But instead of diving into that specifically right now, let's actually get a detail page up. So we've got a route for this index page. Let's go in and add a route for our listings. Great. And then we'll go in and add a new page. Let's call this the slug. View. So we do have a listing there. And maybe I just copy pretty much all of this. We'll actually, what do we wanna keep here? Let's just keep the card component. That's got our information. Maybe we keep the u container just so we get some base styling. And if I wanted to, I can move this header component out of this and into our actual app itself or in probably like the default layout would be a good one as well. Just post that there. So now that will show on every single page. Alright. So as far as our job listing, we're going to fetch that data here. I'm still going to use the read items call, because we're fetching by the slug and not the actual ID. So what we're going to do inside Knox would be something like this, where we use the route composable, or use route composable to get the route. And then we're going to set up a filter for our slug, and we want that to be equal to route. Params.slug. So this should give us our listing. And then what we're going to do here is at the end of this, using the use async data and the I think it's use fetch composable inside Nuxt have an option set where I can transform this data when I return it. So this is going to give me an array and I only want the first item of the array. So even though this only matches one thing because the slug is unique, it still returns an array because of the read items SDK, or the read items method of the SDK. So we're just gonna transform that. And then here inside our card, we don't really need that anymore. Right? So this gives us what we want. Cannot read properties of undefined of slug. Why do we not have this? Right? Here's our listings. There's our slug. Route.params.slug. Directus.requestreaditemsjoblisting. Cannot read properties of undefined of slug. Let's just comment this out, see if the page actually loads. Yes. It does load. Let's figure out why we're not seeing that. So, the Vue dev tools is is really a great tool if you're developing with Vue. We will look for the slug component. Let's see what we got. We got listings. Oh, that's why. That would make sense. Right? Sometimes it's the typos, especially when you're building quickly. They get you into trouble. So here, let's just remove the card. I don't even need this Nuxt link component anymore. Okay. And within our layout, let's make it let's get some navigation so we can actually move backwards here. Make this a Nuxt link. Nuxt link. Change it to 2, do slash. Cool. So now I could go should be able to go back and forth. There we go. Go here. Go there. Amidst options next to header slot. What am I doing wrong there? Alright. Anyway, so this is gonna be our job. And now we have got our job listing. Cannot read properties of type. Alright. Let's go back into Directus. And is this because of our our access control? We've got it. Let's set it up so we can read the files as well. Does that give us what we want? Okay. So at least now I've got some type of card information here. How do we want to display the actual content for this? If we go into one of these, we've got, the job title, the logo here is on the right as well as the company. So we can go in and flesh this out. You know, we could spend a ton of time on design here. I'd rather focus more on the important bits. So let's just do a main component. This is where we'll stuff the content. We could do v dot vhtmlordiv.vhmml, v dash html. That'll be our job dot content. Okay. That should show. Cool. This is not really nicely formatted. Right? So, using Tailwind, they have this really nice pros class we could use to get that formatting. Looks pretty great. Maybe we add some padding to this container. I don't even need that. Strip this away or not some padding. Give it some margin. This looks a little odd here at the top. Let's remove that. We've got Directus is the company. And this could be another NuxLink. This could be, what, to the company? 2/company/job.company.slug. That's what that would look like. And inside this div, maybe we just add some spacing. Okay. Alright. So now we could potentially navigate to the company, but there is no page for that. But here we've got our actual job posting. How do we we want to use like a button for you button. Is it the apply URL? I think I've got application URL here. Apply for this position. Great. What's that gonna do? Job application URL, is that what I set this up as? Let's take a look at our data model. Application URL, job application URL, that should be correct. And we want this to be let's make this a block class. Block. And maybe we drop that below the actual name. We don't have a description for this. It's great. Maybe we do this. Just set off this metadata just a little bit. Okay. We hit apply for this position. And this should open that in a oh, it's because I've got HRF. Let's try this. Apply for this position. That should give me this application page. Great. What is this going to be? In line block? There we go. Okay. So now we've got our page, we've got our company information, we've got our job description. We can drop another button at the bottom of this content. Looking nice. Alright. We are cruising on, like, 20 minutes worth of time. Right? So how can we actually create a post? Right? We've got our post. Let's go back and we'll do a new route. We'll just call it listings dot new. I'm just going to copy the same setup that we've got. Just moving really really quickly. Just drop everything out of here. We're gonna keep Directus. In this case we're gonna use create items. Create item. Great. And on the front end, if I wanted to be able to create a listing, we're going to need to add some navigation for this. So we'll just, let's actually stick this inside our header. Right. Where are you? That's in our default layout. So over here on the far right, we can actually flex these items, justify between. Oh, sorry. I already had the flex. Great. And we'll do listings dot new, post a job. Great. That takes us to the job new page. And on the new page, why do we not okay. Okay. So now we are going to post a job. We've got let's take our same heading here. Post this in here. We've got a create a new job post. Okay. Alright, so now we can start fleshing out our actual form that we want, right? So let's just do form. This could be reactive since we've got some properties here. What all do we have inside that listing? Right. We've got a title. You know, maybe we don't want them to actually choose the slug. We've got a category. That should just be an ID. We got some tags. This is gonna be an array. We got some content. That's gonna be a string. We got an application URL, and we've got a company, which is, it's actually, should be a string, just an ID, but we'll have to actually fetch that information. And then at the end of this, we are going to have a function to submit the form. Alright. Submit listing. And what's that function gonna look like? Oh, let's do a try catch. This is gonna be a constant response equals await, direct us dot request. Create item. Yes. This looks good. Form dot title. Okay. Alright. GitHub Copilot. I got you. And then what are we gonna do? Console log response. And we could send them to like a thank you page or something. Maybe we are tracking like a success state here. And we set success value to true. If there's an error, we're going to log that error. Great. Cool. Alright. So we got submit listing. Now we need to actually build a form. Right? Inside this Nuxt UI library, I think there is a, let's just dive into it, form component. You can even add validation with this using, like, Zod or something like that. But, we've got our form wrapped in a form. There's a form tag. We've got at submit. Let's do dot prevent. Submit listing. That's going to be our form handler. And then inside the form, each one of those gets wrapped in a form group, where we got a label equals title. Placeholder equals, amazing Directus developer. That'll be our placeholder for this. So we that's probably actually on the actual input itself. Alright. And then we have a u input form dot title. We have a placeholder. Let's see what we're getting now. Cool. Amazing Directus developer. Maybe our form isn't so wide. 3 x l. Did that bring it in? It did not. Math would okay. Still a little long, but no worries. Okay. So we've got our title. We want to go through and add the rest of our form. We got a category. Let's see what GitHub Copilot can do here. Yeah. It's not giving us a lot of help here. But we can drop some of these inputs. Maybe we add some spacing between our form components here. So we got tags. We got content. We've got an application URL. Let's make sure we get that. Application URL. And, alright. So for this, like, the company information, the category, etcetera, This could be a list of companies. Right? So what do we have inside Nuxt UI for something like this? We've got a drop down. Is that what we're looking for? Does this have a search? No. That's gonna be input, input menu. I'm assuming this could be an input menu based on the preset. This is searchable. Search attributes, control the query, Async search. Okay. Here we go. So we got a async search function. We can search for a company inside Directus with this. This will be an async function. Let's call it search categories or search companies, maybe. Let's do the company first just to, well, actually, let's do categories. We have them fill out the the company information. Constant, what our data is gonna be await, direct us dot request. That's gonna be read items. We're probably gonna need, what, like, a search query as well. That'll be passed here. This will be our string. Loading value, read items dot categories or categories is gonna be here. Inside that request, we're gonna need a filter. Actually, let's use a search for that. And we'll just do search query. Now the search param here is not optimized like the filter params are, but let's see what we've got here. You form group, you input menu. Oh, wrong one. We want this to be our let's drop this in our category. You input menu, search equals search categories. This is gonna be form dot category. Search for a category. And we can set a loading state for this as well. Alright. Let's see what we've got here. Search for a category. In line properties, not length. If I do this, is it actually searching for a category? Programming? I don't see it. Search search categories, loading, form dot category, placeholder, option name, attribute is a title. Title. Alright. Let's see what else we got here. I'm missing something here. Oh, forgot to return something from that actual function. Gotta return the data, Brian. Makes sense. Alright. See if we got anything else now. Is this actually working? I don't see it calling any of the data, though. Direct is at request, read items, categories. Did I, oh, that's why. Got to import read items as well. Alright, so we refresh. Programming. Okay. Now we see sales and marketing. And inside my actual listing, right, where's the form? So we got our slug. This is actually new. Alright. So we got our form. We got the category. We need the ID of that category. Selecting the actual object. Option Attribute Title. Category dot title? Is that going to give us what we want? Oh, no. Option attribute by ID. Why is it not storing when it selects the value what do we actually want here? V model async search. Use the debounce. You know, we could add a debounce to it. Let's actually deal with that in a moment. We need to actually submit this form first. Alright. For the input menu, what else do we have? Okay. I see like a select menu here that is probably very similar to this. Does this one support multiple objects? It does. So I can select multiple tags for this. This is probably gonna want be what we're looking for there. We can actually probably genericize this as well. We have a string. We have a collection, which is a string. This is gonna be our search. This is gonna be a collection. And in this case, our search would be search categories. What's that gonna be? The categories? Actually, categories. So it'll just be search. Does that work? Is that gonna give us what we need? For some reason, that doesn't work. Undefined. Surf search f n. Do I have, like, another search variable or something? String. Q. Alright. Does that get us what we're looking for? Let's try it again. There we go. Okay. Alright. So now we're getting that. Let's copy in our use select menu. Does this have the same async search capabilities? It does. Our use select menu, this is going to be multiple, so we'll get our tags. The model equals form dot tags. Our search function here is going to look like this. So we get our search functions. This is going to be tags. Search for tags. We've got the option name. It's gonna be a title. That could be multiple. It could be by ID. Great. And now we can see, like, full time, part time. We got some tags selected. Great. Alright. Let's add some buttons, just a single button to our form group. We'll make it type equals submit. And what are we gonna do here? This will just be submit listing. Okay. We've got a job post. Cool. For the sake of this, we've got, like, 7 minutes running on the clock. I am going to allow people to submit a new listing here publicly. I would probably make them log in or create an account first, but, for the sake of just getting this done, let's try it this way. And then here, form dot title dot tags. Let's just see what we're getting first. Here's a new post. When I search for my programming category, we'll apply a couple of tags here. Here's the amazing content for our job post, and we just got a new link. So httpdirectus.i0. If we look at our form component, let me close this. So open this back up. New is the name of this actual component. Why don't I see it? New. There it is. We can see the state of our form. Our tags are an array of objects. Our category is an array of objects. Here, I'm just gonna do this where we do category form dot category dot ID. I think that should work here. For our tags, probably going to have to do some type of mapping dot map tag dot tag dot id. Okay. Oh, no. I lost my amazing post. We are coming up very quickly on the end. We need an amazing Directus developer and a 100 apps, a 100 hours host. Probably after I fail at this one, we'll do design, full time, SEO, select 5 tags. Here's some content for this post. We'll do the direct us URL, and let's see what happens. Alright. We'll do network request. Post. I don't see anything happening. Submit listing, submit listing, type equals submit within the form, Submit listing at submit dot prevent listing. Does this need do I need, like, a click handler on this? I shouldn't. Let's try it anyway. Submit. Why are we not making a single call here? Submit this. Why are we not submitting? Prevent default. At submit dot prevent. And maybe it automatically does that for you. Man. I hate to cut this down to the wire. We are gonna cut it down to the wire. Test. Test. Array of errors. We got what? Internal server error. What do we actually send to the server? Here's our payload tags 12, application URL, category is 2, company is test tag. What kind of response did we get? Why is this an application error? Job listing, submit listing, direct us to request, create item, category. Man. This is frustrating. Form.application.url. Click submit listing. Why isn't this going through? Maybe we unselect these things. Can I unselect 1? Submit listing. Errors. Errors. Errors. Internal server error. Oh, man. There's something that I'm not picking up on. Create item, job listing, title category, form dot ID. What if we just drop these 2? Can we actually get this to submit something? Content. Test. Job listing. We've got the public ability to do the job listing. Job listing tags. Is that what it is maybe? Alright. We're just throwing the Hail Mary pass here. You can edit anything. You can edit anything. You can edit anything. And now we're still getting errors from the direct instance. Internal server error. Is it like a cores issue? You know, if I log out, if I do this in an incognito window, test, test, test. Still getting it right. Items dot job listing response redirected false errors. An unexpected error occurred. What do I have wrong inside this particular setup? Title, status. Is it the slug that I made unique? Slug has to be unique. Is that what it is? Test. I'm going to kick myself after we run out of time on this one. Item, job, listing, post. What what is going on? Is it a a particular, like, a chorus problem? I thought I had chorus set up. Chorus enabled. Chorus origin. This is a oh my gosh, dude. If it is a chorus problem, we're gonna kick myself if it's a course issue. Docker Compose. Let's see what we've got here. I see some logs. Error, insert into job listing. Title created values. Invalid input syntax for ID. Why is it creating an ID? Should not be creating an ID. I don't know what's going on there. Alright. Let's try this again. Test. Test. Test. Error. Error. Error. Database. Into job listing. Invalid syntax input for UUID. Like, UUID should be getting automatically populated though. Application URL. What what am I not sending? Oh, it's the company. It's the company field. Oh my gosh. The company field is killing us there. Okay. Alright. 4 seconds left. Submit. Job Listing. Nothing. Still errored out. Boom. Time exploded on us. Just out of curiosity, it was the company field that was messing things up. Now, we're still getting, now I'm getting like course errors or something. Or, no, does it show it posted? Did we actually get a job listing in here? Yeah, we did. Of course. After time, the company field getting us, I should have like, actually ran that down. Did not. That's the way these things go, man. It's always hairy trying to develop against the clock. Alright. Let's just test this, test with categories. I'm curious if this will actually work now because we have the, now I'm getting cores errors. Alright. This is just me humoring myself at this point. PMPM dev. Let's restart the Docker container. Oh, no. We don't need to do dev. Okay. So we restart this Docker container, fire back up this Nuxt application, see if this is actually going to do something. Just again, me humoring myself here. Correctus is online. Nuxt application is online. Is this actually going to submit? Local host 3,000, post a job, test with categories, can't even spell. Programming, apply some tags. This is a great job. Here's the test. We hit submit. I probably need to add some submission there. You don't have permission to access this. Why don't I have permission to access this? I've got public controls. Tags, content is great. Oh, it's because, the Junction collection. Oh, boy. Yeah. So, tags is actually a should be something like this where I have tags what's this gonna be? Tags underscore ID equals tags ID. Alright. So in that case, now, new is this actually going to be what we want it to be? Full time ID. So when it gets submitted, I'm hoping that should be what we want. Tag underscore ID. Tags underscore ID. No. Save without formatting. What are we doing here? Sometimes you can't win them all. Sometimes you just can't win. Test. Test. Does that actually submit? It does submit with the category. So there we go. Roundabout way of getting to this. We still didn't get exactly where we wanted, but, you know, what did we actually get to here? I blew this one up. So we managed to show a list of jobs. We let users kinda post a job for free. Were these sorted by category? We didn't even do that. Subscribe to job posts, we didn't even get there. Man, alright, that's the way it rolls. Thanks for joining me on this episode of 100 Apps, 100 Hours. I'll catch you on the next one.","acde218a-5b70-4609-9ae5-797787df177b",[470],"0be0daf8-e91d-42cc-9983-359dcf22b4b9",[],{"id":157,"number":158,"show":122,"year":159,"episodes":473},[161,162,163,164,165,166,167,168,169,170],{"id":166,"slug":475,"vimeo_id":476,"description":477,"tile":478,"length":192,"resources":8,"people":8,"episode_number":288,"published":479,"title":480,"video_transcript_html":481,"video_transcript_text":482,"content":8,"seo":483,"status":130,"episode_people":484,"recommendations":486,"season":487},"freelancer-marketplace","937054150","Today, we learn a new acronym \"JFI\"  = Just Fiverr It. Join Bryant as he tries to build a clone of the popular online marketplace for freelancers in just 60 minutes.","bdda03ab-03cc-47d7-b9d8-246c8ddc7fee","2024-05-24","Mission: Freelancer Marketplace","\u003Cp>Speaker 0: Hi, welcome back to another episode of 100 Apps 100 Hours. I'm your host, Brian Gillespie. And in this series, we will be rebuilding or building some of your suggested ideas, some of your favorite apps in 1 hour or less. There's only 2 rules. Number 1, we have 60 minutes to plan and build the application.\u003C/p>\u003Cp>No more, no less. And the second rule is use whatever you have at your disposal. Kind of the anti rule, if you will. So today we are going to be working on a freelancer marketplace. Like some of these ideas we run with on the show, this one sounds kind of vague and generic, but if you take a look at, sites like Upwork or Fiverr is one of the ones that I have kind of just imagined for this.\u003C/p>\u003Cp>Just got a listing of freelancers, a listing of talent that we can work from. On Upwork, you can actually post a job, with an estimated budget and have people bid on that. But I think the more interesting one for me, personally, is having a list of offers on this freelancer marketplace like web scraping via Excel and VBA And having a profile page here that we could purchase this offer and move forward with that as well. So, I think this is kind of what I'm envisioning for this. Let's start the clock and see how far we get with this.\u003C/p>\u003Cp>Alright. So 60 minutes on the clock, we are going to start building this thing, but before we build, we need to plan. So planning wise, I always like to try to discuss or just think out loud what type of functionality I'd like to see from an application. In this case, we want to have a, we need a user profile or a freelancer profile, And we've got a offer, freelancer offer, something like that. We want to be able to have an offer page that customers can buy, and I think that's probably going to get us most of the way there.\u003C/p>\u003Cp>That's really what I want. You know, we can set up this offer and then we need to have a form for them to submit with the details that we need to complete the offer and then complete payment. So how can we get that done? We'll see. Alright.\u003C/p>\u003Cp>So as far as the data model for this application, right, What do we want that to actually look like? So I'm imagining we've got a a user that logs in. That's gonna be actually using our Directus users collection because we are using Directus behind the scenes. Directus gives us users, it gives us authentication, it gives us permissions, and then we have, you know, this could be the customer as well, but then we have a freelancer profile or, you know, we could just call it a seller profile or something like that. Inside that seller profile, we're gonna have some basic information like a name, location, description, etcetera.\u003C/p>\u003Cp>And then attached to that seller profile, we're gonna have the user would be attached to that and then attached to that seller profile, we're probably gonna have some offers. So the offer is gonna be something like the name of the offer, the description. There's probably gonna be a slug on all these things. And then we've got a couple of different offer versions, you know, versions of this offer or packages, something like that. Packages, and those are gonna have a price, deliverables, etcetera.\u003C/p>\u003Cp>So that's the way that I think about this. On the flip side of that, we probably also got, an order or orders. Right? So I ordered this offer, there's a relationship there and a relationship there. And now we have, like, this giant rectangular flow thing going on.\u003C/p>\u003Cp>At least that's how I'm picturing it in my head. So with that in mind, let's get to work on actually building this thing out. The orders are going to have, let's see, they're going to have an offer ID. Offer ID, seller profile, and probably like some form information, form data. I don't know what that's going to be exactly yet, but we'll flesh it out.\u003C/p>\u003Cp>Right? Okay. So let's dive into actually build this application. Alright, I'm going to pull this up side by side. On the setup side, I've just got a Nuxt application we'll use for the front end here and then I've got a blank Directus instance that we're going to start building from.\u003C/p>\u003Cp>So, like I said, we get users out of the box for this. Maybe we go in and we create a new role for a user. They don't have app access but we'll give them access to some collections once we create them. But let's dive in and create our first collection. Let's just call it, our seller profiles, or it could just be profiles.\u003C/p>\u003Cp>You know, it could be, like, we have a buyer profile as well. You know what? Don't second guess it. Let's just roll with it. So we've got a date created, when this was updated, who updated it, the status of this, is this a published profile or not.\u003C/p>\u003Cp>That's great. We'll dive in, we'll add a name for the profile, we'll add a location. I I think this is honestly just like a, maybe it's like a city and a region and a country just so we could, I mean, we could potentially filter on the country if we wanted to. That gives us enough information. All this stuff is being delivered remotely.\u003C/p>\u003Cp>And then we have a description. So description may be HTML content. We want it to be rich, we want people to be able to stylize a description, maybe include some images. So we're going to use our WYSIWYG editor in Directus. Alright, so that's our seller profile.\u003C/p>\u003Cp>It's great, I can go in and set up my seller profile. This is Bry Ross. I am in Bluefield, West Virginia in the United States, just the US. I paint beautiful directus themes for you. Great.\u003C/p>\u003Cp>We may even have a image for that. Right? We've got to have an avatar for our seller profile. We probably have that on the user level as well, but you know, maybe we want to keep that user data private from the general public. So we can attach that to our seller profile and then attach that to our individual user.\u003C/p>\u003Cp>Great. Alright. So let's just search for Brad Ross, see what comes up. That's a great snap. That's what we'll use.\u003C/p>\u003Cp>Away we go. All right, so we've got our seller profile, let's start connecting that, right? That's going to be connected to a user, so we can build in a many to one relationship to that. And honestly, we probably will want to add that relationship to the user as well. Let's go for a one to many setup.\u003C/p>\u003Cp>This is going to be a user. Got the Directus users as the related collection, and the seller profile is going to be the foreign key. We'll show a link to the item. And we're going to make sure this value is unique. So inside Directus, we don't have a one to one relationship, you have to model that using many to one and one to many relationships.\u003C/p>\u003Cp>But you can achieve the same effect by saying, hey, this value has to be unique. So you can see that we are going to create a new profile. Great. Let's see. We've got our user.\u003C/p>\u003Cp>If I go to our system collections, we go to direct us users, I should now also have a seller profile that we can potentially link to. All right. So I've got that relationship set up. Let's go in and create a new orders table, and I'm just gonna leave that blank for now. I'm just gonna get that off of my mind.\u003C/p>\u003Cp>And then we're gonna do offers. Alright. So within our offers we're gonna have the same kind of thing. Who was this created? Is this offer published or not?\u003C/p>\u003Cp>And potentially within a profile maybe there's some offers that we want to sort by. So we can add all of these system fields. You can also just go back and delete those later if you don't need those things. So we've got a name for this offer. We've got, we're probably going to need a slug for this offer.\u003C/p>\u003Cp>And this is an extension that I've installed called the WP WP interface or WP slug extension. It just allows you to add a nice looking slug and you can auto generate this on Create. It doesn't work via the API, but for any heavy lifting inside the admin here, inside the Data Studio, works great. Alright, so again we've got like a rich description for this specific offer that we want to have and then you know, we probably have some packages, right? So what are the different levels of this specific offer?\u003C/p>\u003Cp>You can see here we've got like basic, standard, premium, etcetera. Let's go in and work on that. Right? To me, that would be a one to many relationship because one offer could have many packages, a package is only going to have one offer. So these are going to be constructed on an individual level, on an individual offer or product.\u003C/p>\u003Cp>I'm not sure what Fiverr calls these behind the scenes, but this is what we're going to roll with. We will model this out, we'll get the one to many relationship, we're going to call these our packages or it could be versions. Again, we're not going to get carried away with it. What if we call the Related Collection Offer Packages? So you can see I don't have this collection yet.\u003C/p>\u003Cp>And basically what Directus, the APIs are doing behind the scenes here is just mirroring all these changes to your SQL database. So that's really nice. We've got the offer is going to be our foreign key And I'm going to dig into the advanced field creation mode here. So if I go to my relationship, you can see that Directus is going to create these tables and this field for me, which is really nice, right? I don't have to have that table existing first.\u003C/p>\u003Cp>Directus is just going to create it for me. It makes things simple, especially when I'm just prototyping here. So now I've got packages, and if I back up, we can see that we have offer packages. Those are probably going to be a hidden collection. I don't really need to show those.\u003C/p>\u003Cp>And then we can actually work on the details of the package as well. Right? So we've got a title for the package or name. Package name. Sometimes when I'm working with deeply nested relational data, it helps to prefix like the name or title with the actual name of the collection, just because if you're if you're working with that data in a nested structure, it could be hard to know what the name pertains to, if you don't have some other type of identifier within that.\u003C/p>\u003Cp>So we have a package name, we've got a, like, a delivery window or something, delivery time. I see that's like 3 day, 5 day. We've got revisions. Are we really concerned about that? No.\u003C/p>\u003Cp>We've got like a short description. And in this case, it probably looks like a text description. We'll call it, you know, I could potentially call it short description or let's just call it description. Great. And then we have the deliverables.\u003C/p>\u003Cp>Right? So this could just be a list of things. The Repeater interface inside Directus is kind of nice for that, so that's what we'll use. I can just store this data as JSON information. I just want to store a list of deliverables.\u003C/p>\u003Cp>Deliverables. And then we have the title, the item. That's fine. That's what we'll call it. We'll do full width.\u003C/p>\u003Cp>We'll just make this a simple string, keep it easy. We'll make it an input. And I could do like an icon, but these are just checkboxes here on this site. Great. Alright, so we've got our offer packages, that's linked to an offer.\u003C/p>\u003Cp>Okay. And now on our order level, we need to go in and flesh this out further. Alright, so we've got orders, we've probably got a timestamp for the order. We'll just call it that. Or we could do created at or I think the standard format we use is date created.\u003C/p>\u003Cp>Maybe we use time stamp. It's fine. I can always rename those. And then when I go into the advanced field creation mode, I can set this up so that when an item is created inside this orders table, it will automatically save the current date and time. Okay, great.\u003C/p>\u003Cp>So the timestamp is saved. I can also go in and prevent this from being edited if I need to. That way inside the UI nobody can adjust the timestamp of this order. Alright. So a couple of things.\u003C/p>\u003Cp>The order is going to have a user or customer, you know, let's just call it the direct us users. Great. Alright. And then we have what? We have an offer for this and then we have the package.\u003C/p>\u003Cp>Right? So we're going to have both of those. We have an offer, that's going to be offers as the related collection. We'll save that many to one relationship and then we'll have a package, offer package, just so we make sure we get the correct package from that offer as well. Offer packages.\u003C/p>\u003Cp>Cool. And then, you know, as far as, like, a form after that, you know, maybe we've just got, like, some JSON data that we render. I'm imagining that on the backside of Fiverr, it's like a dynamic form. I could build my own form. So in this case, maybe we just, form data.\u003C/p>\u003Cp>Oh, you we just we just create, like, a JSON field that we could automatically populate data to. Hit save. We'll add a template for it. Okay. So that's what our orders look like.\u003C/p>\u003Cp>I would probably need a an amount for the order. Let's just use a, decimal. Rate. I could go in and flip this around. Maybe we've got 2 decimal places.\u003C/p>\u003Cp>This could be carried out to, I don't know, 10 places. And I could even go in and get fancy and add, like, a dollar sign or something within the interface. Pretty pretty simple. Okay. So offer user amount, form data, that's great.\u003C/p>\u003Cp>What else was the other thing that I wanted to do? On the packages, we forgot to set a price. Alright. So we need a price for this package. Let's use a decimal.\u003C/p>\u003Cp>You could store this as an integer as well. That's what Stripe does, keeps things simple. But, I like decimals displaying nicely inside the UI. Alright. So we got our seller profile.\u003C/p>\u003Cp>Let's create an offer. Right? Why is our offer blank? Oh, some kind of extra table. Okay.\u003C/p>\u003Cp>Alright. So we've got offers. Right? Great. We're gonna I will create a Directus theme for you.\u003C/p>\u003Cp>We'll go ahead and publish this. One thing that I can already tell that we're forgetting is like a gallery for this specific offer. So let me fire up chatgpt. Let's get a nice formatted description here. Create a rich text description for a Fiverr like gig called I will create a Directus theme for you.\u003C/p>\u003Cp>Alright. So while that's working, let's set up our different packages, right? This is the basic package, the delivery time is 3 days. We'll create a simple theme. We've got some deliverables.\u003C/p>\u003Cp>This is, one theme, 2 revisions. Great. Alright. The price for that is 199. And then maybe we add another one.\u003C/p>\u003Cp>Right? So I can also go in and edit this raw value if I wanted to. We'll just go in and add another package. This is the premium package. This takes 5 days.\u003C/p>\u003Cp>We'll create the most amazing theme you have ever seen. One theme. Six revisions. That's a lot of revisions. Let's just dial it back a little bit.\u003C/p>\u003Cp>Way more than what I would actually go to. Here's our description for this and then, you know, hand holding and support. Or let's call it theme installation as well. Cool. Alright.\u003C/p>\u003Cp>We'll copy this description. Paste that in there. Save. Now we've got our offer. I need to add some images to this as well, right?\u003C/p>\u003Cp>So for our offer, maybe we want to show an image gallery. Maybe we have one, there's a featured image that's going to be like a card for this. We'll save that. And then we could potentially also have multiple images. And we'll call this the gallery, image gallery.\u003C/p>\u003Cp>Let's just call it gallery. That's great. And what in this case, what it does is it creates a junction collection, ties that back to our Directus files collection, which is basically your file manager inside Directus. Alright. We've got to have a sort field for those so we can control which order those display.\u003C/p>\u003Cp>And away we go. Right? So now we have a featured image and a gallery. We can upload a feature image. Let's just make this Bry Ross.\u003C/p>\u003Cp>And for our gallery, let's see if we have, like, some screenshots. Yeah. We got some past screenshots here of a few different themes or just one theme, actually. And now we've got an offer. We don't have any orders.\u003C/p>\u003Cp>We've got a seller profile. Cool. Cool. We've got the offer. We don't have that linked to our seller profile yet, right?\u003C/p>\u003Cp>So let's do that as well. This is going to be a mini to 1. This is going to be the seller profile. We'll pick that seller profiles collection. And I'm going to open up the advanced field mode here.\u003C/p>\u003Cp>We're going to add, just this corresponding field in the table Seller Profiles. So we're going to add the reverse relationship there just so we can see that as well. So now if I go into my seller profile for Bry Ross, I should be able to see my offer, but I haven't linked that specific one yet. So we'll go here and pick Bry Ross. There we go, he's got an offer on the marketplace.\u003C/p>\u003Cp>Now if I go back to Bry Ross we can see that actual offer, Vice versa, if I am on the offers I can get to the seller profile. Alright, so that is a lot of data modeling. We've eaten up about 20 minutes of time just fleshing this out. Let's actually start building something. Alright.\u003C/p>\u003Cp>So we'll just start inside the Nuxt application. I'm going to pull this up. Actually it might be handy just to have this kind of sitting here as well. So on the index page, right, we want to get a list of our offers. We can do that.\u003C/p>\u003Cp>I've got a Directus Nuxt plugin here that has some credentials already set up. We've got real time, let's say we wanna view a list of offers. I can do this, I'm gonna access that Directus plug in. So we'll just do Directus equals use Nuxt App. So I'm going to call the Directus client here and then we're going to use the Nuxt Constant Data.\u003C/p>\u003Cp>They have a Use Async Data composable that makes this process easy. So we'll just do offers slash index, give this a key, and then we are going to add a function just to call this data. So we'll do directus dot request, we're going to do read items and we want to pick up the offers collection. We'll add some filters for this. Maybe we want to do the filter equals status.\u003C/p>\u003Cp>We want that to be equal to published, so we only want to get the published offers. And what else are we going to add to this? We need a little bit of help there. Am I missing What am I missing? Got too many.\u003C/p>\u003Cp>Not enough return. Directus offers. Got one more, and we have one more. Use async data, offers index, data, and the mesonoma syntax here. Request.\u003C/p>\u003Cp>Okay. Stuff that in there. There we go. Alright. Got that fixed.\u003C/p>\u003Cp>Now we are actually reading this data. Let's just remove this. We're going to wrap this in a pre tag just to make sure we're actually getting some kind of data from this application. Alright. So what I don't see is any actual data here.\u003C/p>\u003Cp>And if I take a look, we can see if we're getting any API calls. No calls are going out. Number 1, I have to import that read items method from the SDK. Now we can get something going here. On the server side, no, not seeing it.\u003C/p>\u003Cp>One of the potential reasons there is we are not logged in, right? So we created this data model, but Directus gives us a couple different roles for our users or our access control out of the box. By default, the public role is what users have access to when they are not authenticated. So anybody can access this level of information from the API, which is always set to 0 out of the box. So maybe we want to show our offers, when I'm getting ready to move into production I would strictly scope this down.\u003C/p>\u003Cp>Maybe they don't need actual order information, but as far as the publicly displayed information, we're going to show that. And now I can actually see that. Let's just sort this out here. Okay. So we've got like a login and register buttons.\u003C/p>\u003Cp>We don't really need those. Here is our offers. Right? I could drill into the individual packages here, but just to display a list of cards, I don't really need any of that. So we'll just do a quick h two, we'll do some offers, set up a grid.\u003C/p>\u003Cp>How many do they show here across graphic design? Maybe I have 4 up Grid calls, 4. Great. And then we've got, let's just add a card component. The offer for I don't really like any of this.\u003C/p>\u003Cp>Let's destructure this. This is gonna be our offers. Offer and offers. Key. Great.\u003C/p>\u003Cp>Now within our card, we'll just flesh this out a little bit. We'll wrap that in a Nuxt link. So we just want to do 2 offers. I'll have to create a route for this in just a moment. Offer dot ID, or, you know, it could potentially be the slug on the offer if we want.\u003C/p>\u003Cp>Let's roll with ID just for now. Make things a little bit easier. And just close this tag. Great. Okay.\u003C/p>\u003Cp>And then within the NUCs link we have, what, we've got an offer name. So this could be an h three. That's gonna be an offer dot name. Jeez. I can't actually type.\u003C/p>\u003Cp>Then I have a p for the description. Maybe we wanna strip some HTML. Do I have inside my boiler plate here, I should have just a function to strip HTML tags out. So I'll do that. We'll strip HTML.\u003C/p>\u003Cp>Great. And, let's add an image for this as well. Right? So we'll do image. I'm pretty sure I've got this set up with Nuxt Image as well, which is just a replacement for the image tag.\u003C/p>\u003Cp>Allows you to take advantage of, like Sharp library to optimize images. So we got offer dot featured image is the wording we're gonna use here, the offer dot name. Let's see what we've got. Right? Okay.\u003C/p>\u003Cp>So we've got a card, we've got grid calls for, left off an s. That's good. Let's make the font bold for this. Text a little larger, 2 x l. This is way too long as well, so let's truncate the string after we strip all the HTML.\u003C/p>\u003Cp>Maybe we limit to like a 150 characters. Much better. Add just a bit of padding. And I think the Nuxt UI library has this U container component we can use to just add that padding to it. Alright.\u003C/p>\u003Cp>So we got an offer, great. Let's go in, we'll click on that offer, the offer is not found. We need to set up a quick route for this offer, we'll do that. First I'm going to create just a folder for this, Nuxt does file based routing, which is really nice. And here we'll just do the ID dot view and I can copy a lot of this to carry over.\u003C/p>\u003Cp>Alright, so instead of Read Items, I'm just going to Read Item. Now if I was using the slug, I could change my filter to be the slug here. We're going to end up using the route, we need the route params. So we'll just use route. And here, as far as the request, we're gonna change this up just a little bit.\u003C/p>\u003Cp>This will be the offer maybe change this to show the offer ID. Still talking pop up? Yes. Still talking. Okay.\u003C/p>\u003Cp>Offer dot ID. I will, baby. It's the joys of working from home office. Right? Okay.\u003C/p>\u003Cp>So we'll log in. We've got the readitem. Readitem accepts 2 arguments, or actually 3 arguments. We've got the collection, we've got the item ID, and then we have a query that we can use to either return certain fields or not. But in this case, all I'm looking to do here is just grab this ID.\u003C/p>\u003Cp>Cannot access offer before initialization. We don't need any of that. We're not even going to need to wrap this. Right? Offer dot image, This is gonna be the offer dot title.\u003C/p>\u003Cp>Do we actually get the offer here? No. We're missing something. Gonna read the author, read item, cannot access offer before initialization. Oh, the the route dot params dot ID.\u003C/p>\u003Cp>Can't use the offer that you don't have access to yet. Okay. So there we go. We are working with an offer, author, offer. I keep getting those mixed up.\u003C/p>\u003Cp>I'm just gonna go back to this one. I'm gonna drag it over here. I just arced myself, didn't I? As far as the web scraping profile. Okay.\u003C/p>\u003Cp>I've got this on a separate screen that you can't see at the moment. I'm just going to do, like, some quick formatting for this. U container, grid calls. Why does this look this way? Probably because we have it in a grid.\u003C/p>\u003Cp>Set this up. We've got the offer title. Oh, should be the offer name above. That'll actually be an h one tag in this case. Not that you're concerned about semantics on this particular episode.\u003C/p>\u003Cp>Alright. So we've got an image, we've got the description, and in this case, what we're going to do, instead of truncating that, we want to show the whole thing. So we'll do vhmml equals offer dot description and because I've got tailwind installed, I'm just going to use the pros class. We'll use, like, a large text for this. And boom, there we go, we got that.\u003C/p>\u003Cp>Now, on the right side of our page, we've got, like, the offer details, right. But what I don't have in this case, if we just log this data out again, Alright, if I take a look at it, I don't have my package information, I don't have the seller profile that I want. So how can we actually fetch that information? Directus makes this really easy. We'll just use a fields parameter in our query.\u003C/p>\u003Cp>You don't have to, you know, this is available through the regular API, syntaxes. It's pretty much the same across the board here. You don't have to be using the SDK to access this information. So, I don't recommend doing this in production, but because we're just bumping through here, we will use an asterisk to get all the root level fields on this. What else do we want?\u003C/p>\u003Cp>We want the packages. We'll get all the fields on those packages. And then for the gallery, let's get the next level fields for the gallery. And now we can see what we're working with here. So we've got a gallery of images.\u003C/p>\u003Cp>Cool. We've got access to that information. Okay. So I'll just leave this up. That's cool.\u003C/p>\u003Cp>We want to maybe wrap this in another div. We're gonna maybe flex this container. This will be I mean, it it could even be a grid at this stage of the game. Let's make sure we are we're only targeting on, like, a small or a larger screen size. We'll have, like, a I don't know.\u003C/p>\u003Cp>Like a 3 column grid. This will be call span 2 columns. See what that looks like. Do I have this running now? Okay.\u003C/p>\u003Cp>Great. We'll just hide this pre offer. And then on the next one, this will be class call span 1. And here's where we're gonna put our packages. Alright.\u003C/p>\u003Cp>So packages are gonna go over there. Obviously, we're gonna add some nice gap for these just to clean this up a little bit. And, again, this is this is fairly rough. Right? If I want to just move this in here, you could see what do we have as far as our packages?\u003C/p>\u003Cp>Just gonna throw that down there. Alright. So as far as our packages, we've got these shown in a couple tabs. Let's look at ui.nuxt. Do we have tabs here?\u003C/p>\u003Cp>Yeah, there's like a tabs component. Okay. Tabs equals items. Alright. So let's start flushing this out.\u003C/p>\u003Cp>We got tabs. We got items equals, let's just write like an inline function. This is another case of I do as I say, not as I do. But let's actually map these packages out. What have we got here?\u003C/p>\u003Cp>Right? Offer dot packages. We're gonna map these to the actual items. What what shows up in the tab here? The content is x.\u003C/p>\u003Cp>Does this have, like, a oh, it's got, like, a slot that you can use. I to me, this feels, like, really kinda heavy handed, but, yeah. Whatever. Let's roll with it. Packages dot mat.\u003C/p>\u003Cp>Actually, let's not. Let's just like flesh these out real quick. Divv4. We use, like, a selected package. Selected package.\u003C/p>\u003Cp>That'll be what's offer dot packages offer.value.packages. And we'll do the first one. Alright. So that's gonna be our selected package dot ID. Let's call it selected package ID.\u003C/p>\u003Cp>Good enough. Alright. And then we're just gonna loop through these packages, offer dot packages, offer in offer dot packages. The key here would be key equals offer or, actually, no. We want package Package dot ID.\u003C/p>\u003Cp>Okay. How are we doing on time? We'll check this. We have got 20 minutes left. Package dot ID.\u003C/p>\u003Cp>Alright. And then within the package, we are going to have the package. Name, package dot name. Let's just see what this gives us. Then we have a list of deliverables.\u003C/p>\u003Cp>V 4 package alright. Deliverable item in package dot deliverables. Key equals we'll just call that the item, and we'll do item dot item. K. Item dotitem.\u003C/p>\u003Cp>K. What else do we have? We have a short description for that. That'd be a p tag. Package dot description.\u003C/p>\u003Cp>Alright. And, oh, transform failed with, like, 5 things. We've got just too much going on there. There's our packages. Those are not showing.\u003C/p>\u003Cp>Offer in packages. Package in offer dot packages. Key equals package dot ID. Offer package, oh, package underscore name, package description. Oh, we can't use package.\u003C/p>\u003Cp>That doesn't make a ton of sense, but, it does make sense. Pkg offer dot packages. Should learn that a long time ago. Item in pkg. Cannot read properties of dot packages, null.\u003C/p>\u003Cp>Offer dot value. Okay. Alright. So there's our different packages. We probably got a price for that as well.\u003C/p>\u003Cp>Let's take a look. Cool. I was in a div.flex p dollar sign pkg.price199. Okay. Package dot.\u003C/p>\u003Cp>It's actually package name. Okay. How are we doing on time? Probably not good. And then we'll just justify between.\u003C/p>\u003Cp>Okay. And what else? So we got our packages. Let's wrap this. Actually, we wanna do this.\u003C/p>\u003Cp>We'll do wrap it in a template tag. V if, we get selected packages, selected package ID, offer dot value, unref, offer packages, 0 dot idvif, what, selected package ID equals package.id. I can't read no packages. So this doesn't exist yet. Select the package ID.\u003C/p>\u003Cp>Offer dot packages. What are we gonna do with that? Let's just leave that. Null for now. That'll get us back to rendering, but we don't really render anything.\u003C/p>\u003Cp>And we do if offer what's that gonna do for us? Can't read, undefined, offer dot packages. Oh, did I even have that correct to begin with? Unref offer dot packages dot zero dot ID dotvalue.packages.id. Okay.\u003C/p>\u003Cp>Anyway Alright. And then what we're gonna do, we'll just add a list of buttons here. Div. Flex those. Add some gap to alright.\u003C/p>\u003Cp>So we'll do buttonv4, package and offer dot packages. And package dot package name. Alright. So we got those 2. And on the click of the button, that click, we'll do selected package ID equals pkg.id.\u003C/p>\u003Cp>No. Why is it doing that? Let's see. Expanding this when I don't want it to. Thank you, GitHub Copilot.\u003C/p>\u003Cp>Alright. So there we go. We can switch between these two packages. And then for each package, maybe we just have a buy button for that as well. Let's see what this looks like inside, Fiverr.\u003C/p>\u003Cp>They kinda send us out where you can order the quantity, you can change, you can add on, things like that. Not super concerned with that. I'm actually going to just totally cheat here. We'll open up my old faithful Stripe account, log in, Go into test mode for this. Just create a new product.\u003C/p>\u003Cp>We'll call this the theming package, theming offer, direct to themes, buy Bry. Great. We've got more pricing options. This is gonna be a one off. We've got flat rate, customer chooses price, price by package, or bundle of packages.\u003C/p>\u003Cp>Let's do a flat rate. Hit next. What did we charge for this? 199 for the one package. Do set.\u003C/p>\u003Cp>Add another price. This will be the 299 for the premium package. Great. Billing period. That's a flat rate.\u003C/p>\u003Cp>Great. Alright. So we got 2 potential options there. We'll add this product. Of course, I gotta log in.\u003C/p>\u003Cp>Alright. We're cooking. We're cooking. We got 15 minutes left. And now direct this theme is by Bry.\u003C/p>\u003Cp>You know, normally, like, taking the time to go through this, I would certainly go in and then, just basically, like, have this created through a Stripe checkout. So we got a payment link that we'll create for this. Great. We'll create this link, copy this, and if we go into our direct assistance where are you, mister directus instance? Admin@example.com.\u003C/p>\u003Cp>Password. Okay. Alright. So for each one of these packages, offered packages, let's have a buy link. That'll just be a string.\u003C/p>\u003Cp>We'll serve that on the front end inside a button component. That looks good. Alright. Offer packages. So let's set this up.\u003C/p>\u003Cp>Right? This is our offer. Where's our packages? Here's the 199 package. There's the buy link for that.\u003C/p>\u003Cp>Lost my Stripe payment link. We'll create a let's create another payment link real quick. Where are you? Directus themes by Bri. We need a payment link for this one.\u003C/p>\u003Cp>Create the link. Great. Copy that. Again, this is the quick and dirty way of doing this, but sometimes you got to. Alright.\u003C/p>\u003Cp>This will be 299 for this specific package. Save. Save. Save. Save.\u003C/p>\u003Cp>If we look local host, now we should have access to that data. So we can see those prices there. And then inside each one of these packages, after our deliverables, we'll just add a button. We'll add a 2. This is gonna be, package pkg.buyurl.\u003C/p>\u003Cp>Is that what I set that up as? By link. By underscore link. By pkgpackage_name. And by premium, this should redirect us.\u003C/p>\u003Cp>Right? Redirect us to direct us. Yeah. Nothing fancy here. Alright.\u003C/p>\u003Cp>So if I hit now, I hit buy premium. We've got what? 13 minutes left? Buy premium. I go in, I check out, that's great, we're gonna have to fill out this information.\u003C/p>\u003Cp>But one thing I wanna do here afterwards, right, is after they pay for this, we need to send them to a form. Now if I'm using the Stripe API endpoint, I can set up a referral URL after I check out to redirect. Not going to do that here because I don't really have time to set up all those specific endpoints. Again, we're gonna go quick and dirty mode. We'll go to webhooks.\u003C/p>\u003Cp>Let's set up a webhook for this. Of course, this is gonna be in my local environment as well. Okay. I gonna cut this close. Right?\u003C/p>\u003Cp>We'll go into flows. Let's create a new flow. We'll call it catch stripe. Webhook. Catch stripe will trigger on a webhook.\u003C/p>\u003Cp>Incoming webhook, it will be a post request. What are we going to do when we catch that? Are are am I I I'm I'm getting way ahead of myself. Like, we could catch this and do an email or something like that, but let's just say we yeah. It let's let's do something more interesting.\u003C/p>\u003Cp>And, switching gears here. Let's just assume gonna make a bunch of assumptions here. I'm just gonna go ahead and check out in this process. If we go back to Stripe where are you, Stripe? I think within the actual payment links, can we do this by, let's say for the premium link, after payment we are going to just redirect.\u003C/p>\u003Cp>We're gonna go to http local host 3,000/submit/offer/submit. Offers slash submit. Alright. So we're just gonna create a new page. This will be submit dot view.\u003C/p>\u003Cp>We'll take this information. Submit dot view. And what are we gonna do with this? Right? We're gonna make sure we still let's let's get our offer just to show that up at the top.\u003C/p>\u003Cp>And then we're gonna build a form to actually submit some data or actually create an order. Again, normally all this would be via webhook and either flows or like a specific endpoint, but this is what we'll do just to throw something out with our remaining 10 minutes. Right? So we've got our freelancer profile. We've got an offer page that customers can buy.\u003C/p>\u003Cp>So we check the box on that functionality. Technically, I guess this is a win. Is it, a smooth win? I don't know. And then we just want to, like, submit an order form.\u003C/p>\u003Cp>Alright. So how do we do that? We will do create item. We're gonna need read item. We still wanna pick up that offer.\u003C/p>\u003Cp>And let's make sure this submit isn't gonna actually work on our front end. Got too many links going on. Local host 3,000 slash offers slash submit. Cannot read properties. Offer oh, I don't have the route params.\u003C/p>\u003Cp>So let's drop that. Let's move offers underscore ID. We'll make that a folder. This is gonna be dropped into that folder. So it's submit.\u003C/p>\u003Cp>And this is gonna be index. Alright. So that adjusts things a little bit. And now offers route params.id. Oh, becomes let's go back to the index page.\u003C/p>\u003Cp>We go here now. Actually, that's gonna be broken. And it no. That won't be broken. Shouldn't be broken.\u003C/p>\u003Cp>There we go. Alright. So now I've got that, and then we'll just have a submit page. There we go. We've got that.\u003C/p>\u003Cp>Let's add a h two tag. Thanks for your purchase of this package. Great. And then we've got, like, a form. Alright.\u003C/p>\u003Cp>So u form, and what are we gonna do with this form? We are, what, we're going to submit this, we're going to need a function here, async function. Submit order. K. We are going to wrap that in a try catch.\u003C/p>\u003Cp>We'll do constant order equals await, direct us create offers, create orders, create item orders. We have the package, selected package dot ID dot value. Let's not worry about the package at this moment. Then we've got form data, which is just gonna be a JSON object. What else do we have inside this Directus instance that keeps magically getting logged out?\u003C/p>\u003Cp>7 minutes, offers, orders. Alright. So we had the user. We had the amount. That's gonna be the selected package.\u003C/p>\u003Cp>This is probably actually backwards. Right? Maybe we should have submitted this form first and then redirected to checkout after they submitted this form. But, hey. Nobody's perfect.\u003C/p>\u003Cp>Alright. So form data. What do we want inside the form data? We don't really have to worry about that at this moment. Then we'll just add one more.\u003C/p>\u003Cp>There's our submit order function. Will this create an order? We can console. Log the order after we create it. But let's just build a quick form that will actually submit this.\u003C/p>\u003Cp>Alright. So we got you form group. What do we need to actually build a theme for someone. So we need the website URL. We'll have a input for that.\u003C/p>\u003Cp>V model equals form dot website URL. Actually, that'll be form under underscore data. I actually need a form here. Form, let's make this reactive. And, honestly, here, this will be form.\u003C/p>\u003Cp>Alright. So form dot website URL. Great. Make that required. And I think that actually goes here on the form group.\u003C/p>\u003Cp>Again, just rolling off of the Nuxt setup. Label, what else do we want? Form. I got like 35 things going wrong here. Theme, colors.\u003C/p>\u003Cp>Maybe we add, like, some kind of extra helper text within this. What is the color palette for this theme. Okay. Let's see what this actually looks like. Okay.\u003C/p>\u003Cp>So we got like a we've got an actual form rolling here. Add some spacing. Add a submit button, new button. Submit form. Okay.\u003C/p>\u003Cp>Also, let's make sure that you have to be logged in for this. So we'll do constant user equals use state user v if user u form v else. Oh, no. I don't need this. Div v else.\u003C/p>\u003Cp>Please log in to submit the form. We'll do u button. Ah, boy. Actually, it would be just 2/login. No.\u003C/p>\u003Cp>It's auth/login. Log in. Alright. Are we actually logged in? I guess I am logged in because I'm sharing a session token here.\u003C/p>\u003Cp>But if I were to like, go here and look at this, right, it's not gonna show me that because I'm not logged in. And then I could log in, and then I could go back and potentially see that offer or submit. Right. So now that I'm logged in, I can see this information. But we are running dangerously low on time.\u003C/p>\u003Cp>Let's make this a text area. And let's just run through this. Right? We have purchased something via the offer page. We're now submitting this data.\u003C/p>\u003Cp>What a mess this one's turned into. But we'll do the directis. Io, violet, white, shades of pink, slight gray. Cool. We hit submit form.\u003C/p>\u003Cp>What happens what happens if I hit submit form? Nothing happens, because I didn't tie that to any actual button. At submit equals what submit order. Submit order. Submit order.\u003C/p>\u003Cp>I should be seeing a fetch request. At button oh, it's a click handler. At submit would have been on the form itself. We'll save. Website URL, httpdirectus.io.\u003C/p>\u003Cp>Okay. Do we wanna track, like, a success state? Success equals ref false. I love cutting this down to the wire. Right?\u003C/p>\u003Cp>Console log order, Success value equals true. And then if the and no success. Maybe we want to show a success message. The if success. We do yes.\u003C/p>\u003Cp>Okay. What are we looking at here? Why is it not logged in? V else if v else if. V else.\u003C/p>\u003Cp>So I fix it. Okay. HTTPS, 1 minute 25 seconds. What happens? Nothing is happening.\u003C/p>\u003Cp>What did I do? What happened to my click handler? Disappeared. Submit order. I got the wrong I put it in the wrong spot.\u003C/p>\u003Cp>Okay. Let's try it again. Refresh. Httpdirectus.io. Where is my success with 48 seconds left to spare.\u003C/p>\u003Cp>Has this actually gone through? Has it submitted an order? Can we actually look at it? Do we have an order inside the system? We do.\u003C/p>\u003Cp>We don't have the offer and the package in there. So the offer equals the value dot author dot ID. If we go back and we check that just one more time, Submit another order to this. Will that now have the there's the offer. Again, like, we could have this all set up and routed correctly with enough time, but what a wild ride that was to try and get this information actually into the system.\u003C/p>\u003Cp>So wow. Where would we take this from here? Right? I would go through and set up this logic to be very robust. Again, as far as the offers here, I would probably I don't know that I would create products inside the Stripe account for each one of these offers.\u003C/p>\u003Cp>I would just set up, a checkout to Stripe that would take the order value or the order amount from a cart, throw that to the Stripe checkout, they would check out. Stripe sends a webhook. We also redirect them back to a page where they can fill out the necessary information, on a page like this to give us the order information that we need, to actually complete that order. So we can see that information here. But that's where I would go from here.\u003C/p>\u003Cp>Obviously, I would do a little a few things different. If we had to add more than an hour, probably 2 hours here, we could have had a really nice application. But bare bones, this is pretty good. That's it for this episode of 100 apps, 100 hours. I'll catch you on the next one.\u003C/p>\u003Cp>Thanks for following along.\u003C/p>","Hi, welcome back to another episode of 100 Apps 100 Hours. I'm your host, Brian Gillespie. And in this series, we will be rebuilding or building some of your suggested ideas, some of your favorite apps in 1 hour or less. There's only 2 rules. Number 1, we have 60 minutes to plan and build the application. No more, no less. And the second rule is use whatever you have at your disposal. Kind of the anti rule, if you will. So today we are going to be working on a freelancer marketplace. Like some of these ideas we run with on the show, this one sounds kind of vague and generic, but if you take a look at, sites like Upwork or Fiverr is one of the ones that I have kind of just imagined for this. Just got a listing of freelancers, a listing of talent that we can work from. On Upwork, you can actually post a job, with an estimated budget and have people bid on that. But I think the more interesting one for me, personally, is having a list of offers on this freelancer marketplace like web scraping via Excel and VBA And having a profile page here that we could purchase this offer and move forward with that as well. So, I think this is kind of what I'm envisioning for this. Let's start the clock and see how far we get with this. Alright. So 60 minutes on the clock, we are going to start building this thing, but before we build, we need to plan. So planning wise, I always like to try to discuss or just think out loud what type of functionality I'd like to see from an application. In this case, we want to have a, we need a user profile or a freelancer profile, And we've got a offer, freelancer offer, something like that. We want to be able to have an offer page that customers can buy, and I think that's probably going to get us most of the way there. That's really what I want. You know, we can set up this offer and then we need to have a form for them to submit with the details that we need to complete the offer and then complete payment. So how can we get that done? We'll see. Alright. So as far as the data model for this application, right, What do we want that to actually look like? So I'm imagining we've got a a user that logs in. That's gonna be actually using our Directus users collection because we are using Directus behind the scenes. Directus gives us users, it gives us authentication, it gives us permissions, and then we have, you know, this could be the customer as well, but then we have a freelancer profile or, you know, we could just call it a seller profile or something like that. Inside that seller profile, we're gonna have some basic information like a name, location, description, etcetera. And then attached to that seller profile, we're gonna have the user would be attached to that and then attached to that seller profile, we're probably gonna have some offers. So the offer is gonna be something like the name of the offer, the description. There's probably gonna be a slug on all these things. And then we've got a couple of different offer versions, you know, versions of this offer or packages, something like that. Packages, and those are gonna have a price, deliverables, etcetera. So that's the way that I think about this. On the flip side of that, we probably also got, an order or orders. Right? So I ordered this offer, there's a relationship there and a relationship there. And now we have, like, this giant rectangular flow thing going on. At least that's how I'm picturing it in my head. So with that in mind, let's get to work on actually building this thing out. The orders are going to have, let's see, they're going to have an offer ID. Offer ID, seller profile, and probably like some form information, form data. I don't know what that's going to be exactly yet, but we'll flesh it out. Right? Okay. So let's dive into actually build this application. Alright, I'm going to pull this up side by side. On the setup side, I've just got a Nuxt application we'll use for the front end here and then I've got a blank Directus instance that we're going to start building from. So, like I said, we get users out of the box for this. Maybe we go in and we create a new role for a user. They don't have app access but we'll give them access to some collections once we create them. But let's dive in and create our first collection. Let's just call it, our seller profiles, or it could just be profiles. You know, it could be, like, we have a buyer profile as well. You know what? Don't second guess it. Let's just roll with it. So we've got a date created, when this was updated, who updated it, the status of this, is this a published profile or not. That's great. We'll dive in, we'll add a name for the profile, we'll add a location. I I think this is honestly just like a, maybe it's like a city and a region and a country just so we could, I mean, we could potentially filter on the country if we wanted to. That gives us enough information. All this stuff is being delivered remotely. And then we have a description. So description may be HTML content. We want it to be rich, we want people to be able to stylize a description, maybe include some images. So we're going to use our WYSIWYG editor in Directus. Alright, so that's our seller profile. It's great, I can go in and set up my seller profile. This is Bry Ross. I am in Bluefield, West Virginia in the United States, just the US. I paint beautiful directus themes for you. Great. We may even have a image for that. Right? We've got to have an avatar for our seller profile. We probably have that on the user level as well, but you know, maybe we want to keep that user data private from the general public. So we can attach that to our seller profile and then attach that to our individual user. Great. Alright. So let's just search for Brad Ross, see what comes up. That's a great snap. That's what we'll use. Away we go. All right, so we've got our seller profile, let's start connecting that, right? That's going to be connected to a user, so we can build in a many to one relationship to that. And honestly, we probably will want to add that relationship to the user as well. Let's go for a one to many setup. This is going to be a user. Got the Directus users as the related collection, and the seller profile is going to be the foreign key. We'll show a link to the item. And we're going to make sure this value is unique. So inside Directus, we don't have a one to one relationship, you have to model that using many to one and one to many relationships. But you can achieve the same effect by saying, hey, this value has to be unique. So you can see that we are going to create a new profile. Great. Let's see. We've got our user. If I go to our system collections, we go to direct us users, I should now also have a seller profile that we can potentially link to. All right. So I've got that relationship set up. Let's go in and create a new orders table, and I'm just gonna leave that blank for now. I'm just gonna get that off of my mind. And then we're gonna do offers. Alright. So within our offers we're gonna have the same kind of thing. Who was this created? Is this offer published or not? And potentially within a profile maybe there's some offers that we want to sort by. So we can add all of these system fields. You can also just go back and delete those later if you don't need those things. So we've got a name for this offer. We've got, we're probably going to need a slug for this offer. And this is an extension that I've installed called the WP WP interface or WP slug extension. It just allows you to add a nice looking slug and you can auto generate this on Create. It doesn't work via the API, but for any heavy lifting inside the admin here, inside the Data Studio, works great. Alright, so again we've got like a rich description for this specific offer that we want to have and then you know, we probably have some packages, right? So what are the different levels of this specific offer? You can see here we've got like basic, standard, premium, etcetera. Let's go in and work on that. Right? To me, that would be a one to many relationship because one offer could have many packages, a package is only going to have one offer. So these are going to be constructed on an individual level, on an individual offer or product. I'm not sure what Fiverr calls these behind the scenes, but this is what we're going to roll with. We will model this out, we'll get the one to many relationship, we're going to call these our packages or it could be versions. Again, we're not going to get carried away with it. What if we call the Related Collection Offer Packages? So you can see I don't have this collection yet. And basically what Directus, the APIs are doing behind the scenes here is just mirroring all these changes to your SQL database. So that's really nice. We've got the offer is going to be our foreign key And I'm going to dig into the advanced field creation mode here. So if I go to my relationship, you can see that Directus is going to create these tables and this field for me, which is really nice, right? I don't have to have that table existing first. Directus is just going to create it for me. It makes things simple, especially when I'm just prototyping here. So now I've got packages, and if I back up, we can see that we have offer packages. Those are probably going to be a hidden collection. I don't really need to show those. And then we can actually work on the details of the package as well. Right? So we've got a title for the package or name. Package name. Sometimes when I'm working with deeply nested relational data, it helps to prefix like the name or title with the actual name of the collection, just because if you're if you're working with that data in a nested structure, it could be hard to know what the name pertains to, if you don't have some other type of identifier within that. So we have a package name, we've got a, like, a delivery window or something, delivery time. I see that's like 3 day, 5 day. We've got revisions. Are we really concerned about that? No. We've got like a short description. And in this case, it probably looks like a text description. We'll call it, you know, I could potentially call it short description or let's just call it description. Great. And then we have the deliverables. Right? So this could just be a list of things. The Repeater interface inside Directus is kind of nice for that, so that's what we'll use. I can just store this data as JSON information. I just want to store a list of deliverables. Deliverables. And then we have the title, the item. That's fine. That's what we'll call it. We'll do full width. We'll just make this a simple string, keep it easy. We'll make it an input. And I could do like an icon, but these are just checkboxes here on this site. Great. Alright, so we've got our offer packages, that's linked to an offer. Okay. And now on our order level, we need to go in and flesh this out further. Alright, so we've got orders, we've probably got a timestamp for the order. We'll just call it that. Or we could do created at or I think the standard format we use is date created. Maybe we use time stamp. It's fine. I can always rename those. And then when I go into the advanced field creation mode, I can set this up so that when an item is created inside this orders table, it will automatically save the current date and time. Okay, great. So the timestamp is saved. I can also go in and prevent this from being edited if I need to. That way inside the UI nobody can adjust the timestamp of this order. Alright. So a couple of things. The order is going to have a user or customer, you know, let's just call it the direct us users. Great. Alright. And then we have what? We have an offer for this and then we have the package. Right? So we're going to have both of those. We have an offer, that's going to be offers as the related collection. We'll save that many to one relationship and then we'll have a package, offer package, just so we make sure we get the correct package from that offer as well. Offer packages. Cool. And then, you know, as far as, like, a form after that, you know, maybe we've just got, like, some JSON data that we render. I'm imagining that on the backside of Fiverr, it's like a dynamic form. I could build my own form. So in this case, maybe we just, form data. Oh, you we just we just create, like, a JSON field that we could automatically populate data to. Hit save. We'll add a template for it. Okay. So that's what our orders look like. I would probably need a an amount for the order. Let's just use a, decimal. Rate. I could go in and flip this around. Maybe we've got 2 decimal places. This could be carried out to, I don't know, 10 places. And I could even go in and get fancy and add, like, a dollar sign or something within the interface. Pretty pretty simple. Okay. So offer user amount, form data, that's great. What else was the other thing that I wanted to do? On the packages, we forgot to set a price. Alright. So we need a price for this package. Let's use a decimal. You could store this as an integer as well. That's what Stripe does, keeps things simple. But, I like decimals displaying nicely inside the UI. Alright. So we got our seller profile. Let's create an offer. Right? Why is our offer blank? Oh, some kind of extra table. Okay. Alright. So we've got offers. Right? Great. We're gonna I will create a Directus theme for you. We'll go ahead and publish this. One thing that I can already tell that we're forgetting is like a gallery for this specific offer. So let me fire up chatgpt. Let's get a nice formatted description here. Create a rich text description for a Fiverr like gig called I will create a Directus theme for you. Alright. So while that's working, let's set up our different packages, right? This is the basic package, the delivery time is 3 days. We'll create a simple theme. We've got some deliverables. This is, one theme, 2 revisions. Great. Alright. The price for that is 199. And then maybe we add another one. Right? So I can also go in and edit this raw value if I wanted to. We'll just go in and add another package. This is the premium package. This takes 5 days. We'll create the most amazing theme you have ever seen. One theme. Six revisions. That's a lot of revisions. Let's just dial it back a little bit. Way more than what I would actually go to. Here's our description for this and then, you know, hand holding and support. Or let's call it theme installation as well. Cool. Alright. We'll copy this description. Paste that in there. Save. Now we've got our offer. I need to add some images to this as well, right? So for our offer, maybe we want to show an image gallery. Maybe we have one, there's a featured image that's going to be like a card for this. We'll save that. And then we could potentially also have multiple images. And we'll call this the gallery, image gallery. Let's just call it gallery. That's great. And what in this case, what it does is it creates a junction collection, ties that back to our Directus files collection, which is basically your file manager inside Directus. Alright. We've got to have a sort field for those so we can control which order those display. And away we go. Right? So now we have a featured image and a gallery. We can upload a feature image. Let's just make this Bry Ross. And for our gallery, let's see if we have, like, some screenshots. Yeah. We got some past screenshots here of a few different themes or just one theme, actually. And now we've got an offer. We don't have any orders. We've got a seller profile. Cool. Cool. We've got the offer. We don't have that linked to our seller profile yet, right? So let's do that as well. This is going to be a mini to 1. This is going to be the seller profile. We'll pick that seller profiles collection. And I'm going to open up the advanced field mode here. We're going to add, just this corresponding field in the table Seller Profiles. So we're going to add the reverse relationship there just so we can see that as well. So now if I go into my seller profile for Bry Ross, I should be able to see my offer, but I haven't linked that specific one yet. So we'll go here and pick Bry Ross. There we go, he's got an offer on the marketplace. Now if I go back to Bry Ross we can see that actual offer, Vice versa, if I am on the offers I can get to the seller profile. Alright, so that is a lot of data modeling. We've eaten up about 20 minutes of time just fleshing this out. Let's actually start building something. Alright. So we'll just start inside the Nuxt application. I'm going to pull this up. Actually it might be handy just to have this kind of sitting here as well. So on the index page, right, we want to get a list of our offers. We can do that. I've got a Directus Nuxt plugin here that has some credentials already set up. We've got real time, let's say we wanna view a list of offers. I can do this, I'm gonna access that Directus plug in. So we'll just do Directus equals use Nuxt App. So I'm going to call the Directus client here and then we're going to use the Nuxt Constant Data. They have a Use Async Data composable that makes this process easy. So we'll just do offers slash index, give this a key, and then we are going to add a function just to call this data. So we'll do directus dot request, we're going to do read items and we want to pick up the offers collection. We'll add some filters for this. Maybe we want to do the filter equals status. We want that to be equal to published, so we only want to get the published offers. And what else are we going to add to this? We need a little bit of help there. Am I missing What am I missing? Got too many. Not enough return. Directus offers. Got one more, and we have one more. Use async data, offers index, data, and the mesonoma syntax here. Request. Okay. Stuff that in there. There we go. Alright. Got that fixed. Now we are actually reading this data. Let's just remove this. We're going to wrap this in a pre tag just to make sure we're actually getting some kind of data from this application. Alright. So what I don't see is any actual data here. And if I take a look, we can see if we're getting any API calls. No calls are going out. Number 1, I have to import that read items method from the SDK. Now we can get something going here. On the server side, no, not seeing it. One of the potential reasons there is we are not logged in, right? So we created this data model, but Directus gives us a couple different roles for our users or our access control out of the box. By default, the public role is what users have access to when they are not authenticated. So anybody can access this level of information from the API, which is always set to 0 out of the box. So maybe we want to show our offers, when I'm getting ready to move into production I would strictly scope this down. Maybe they don't need actual order information, but as far as the publicly displayed information, we're going to show that. And now I can actually see that. Let's just sort this out here. Okay. So we've got like a login and register buttons. We don't really need those. Here is our offers. Right? I could drill into the individual packages here, but just to display a list of cards, I don't really need any of that. So we'll just do a quick h two, we'll do some offers, set up a grid. How many do they show here across graphic design? Maybe I have 4 up Grid calls, 4. Great. And then we've got, let's just add a card component. The offer for I don't really like any of this. Let's destructure this. This is gonna be our offers. Offer and offers. Key. Great. Now within our card, we'll just flesh this out a little bit. We'll wrap that in a Nuxt link. So we just want to do 2 offers. I'll have to create a route for this in just a moment. Offer dot ID, or, you know, it could potentially be the slug on the offer if we want. Let's roll with ID just for now. Make things a little bit easier. And just close this tag. Great. Okay. And then within the NUCs link we have, what, we've got an offer name. So this could be an h three. That's gonna be an offer dot name. Jeez. I can't actually type. Then I have a p for the description. Maybe we wanna strip some HTML. Do I have inside my boiler plate here, I should have just a function to strip HTML tags out. So I'll do that. We'll strip HTML. Great. And, let's add an image for this as well. Right? So we'll do image. I'm pretty sure I've got this set up with Nuxt Image as well, which is just a replacement for the image tag. Allows you to take advantage of, like Sharp library to optimize images. So we got offer dot featured image is the wording we're gonna use here, the offer dot name. Let's see what we've got. Right? Okay. So we've got a card, we've got grid calls for, left off an s. That's good. Let's make the font bold for this. Text a little larger, 2 x l. This is way too long as well, so let's truncate the string after we strip all the HTML. Maybe we limit to like a 150 characters. Much better. Add just a bit of padding. And I think the Nuxt UI library has this U container component we can use to just add that padding to it. Alright. So we got an offer, great. Let's go in, we'll click on that offer, the offer is not found. We need to set up a quick route for this offer, we'll do that. First I'm going to create just a folder for this, Nuxt does file based routing, which is really nice. And here we'll just do the ID dot view and I can copy a lot of this to carry over. Alright, so instead of Read Items, I'm just going to Read Item. Now if I was using the slug, I could change my filter to be the slug here. We're going to end up using the route, we need the route params. So we'll just use route. And here, as far as the request, we're gonna change this up just a little bit. This will be the offer maybe change this to show the offer ID. Still talking pop up? Yes. Still talking. Okay. Offer dot ID. I will, baby. It's the joys of working from home office. Right? Okay. So we'll log in. We've got the readitem. Readitem accepts 2 arguments, or actually 3 arguments. We've got the collection, we've got the item ID, and then we have a query that we can use to either return certain fields or not. But in this case, all I'm looking to do here is just grab this ID. Cannot access offer before initialization. We don't need any of that. We're not even going to need to wrap this. Right? Offer dot image, This is gonna be the offer dot title. Do we actually get the offer here? No. We're missing something. Gonna read the author, read item, cannot access offer before initialization. Oh, the the route dot params dot ID. Can't use the offer that you don't have access to yet. Okay. So there we go. We are working with an offer, author, offer. I keep getting those mixed up. I'm just gonna go back to this one. I'm gonna drag it over here. I just arced myself, didn't I? As far as the web scraping profile. Okay. I've got this on a separate screen that you can't see at the moment. I'm just going to do, like, some quick formatting for this. U container, grid calls. Why does this look this way? Probably because we have it in a grid. Set this up. We've got the offer title. Oh, should be the offer name above. That'll actually be an h one tag in this case. Not that you're concerned about semantics on this particular episode. Alright. So we've got an image, we've got the description, and in this case, what we're going to do, instead of truncating that, we want to show the whole thing. So we'll do vhmml equals offer dot description and because I've got tailwind installed, I'm just going to use the pros class. We'll use, like, a large text for this. And boom, there we go, we got that. Now, on the right side of our page, we've got, like, the offer details, right. But what I don't have in this case, if we just log this data out again, Alright, if I take a look at it, I don't have my package information, I don't have the seller profile that I want. So how can we actually fetch that information? Directus makes this really easy. We'll just use a fields parameter in our query. You don't have to, you know, this is available through the regular API, syntaxes. It's pretty much the same across the board here. You don't have to be using the SDK to access this information. So, I don't recommend doing this in production, but because we're just bumping through here, we will use an asterisk to get all the root level fields on this. What else do we want? We want the packages. We'll get all the fields on those packages. And then for the gallery, let's get the next level fields for the gallery. And now we can see what we're working with here. So we've got a gallery of images. Cool. We've got access to that information. Okay. So I'll just leave this up. That's cool. We want to maybe wrap this in another div. We're gonna maybe flex this container. This will be I mean, it it could even be a grid at this stage of the game. Let's make sure we are we're only targeting on, like, a small or a larger screen size. We'll have, like, a I don't know. Like a 3 column grid. This will be call span 2 columns. See what that looks like. Do I have this running now? Okay. Great. We'll just hide this pre offer. And then on the next one, this will be class call span 1. And here's where we're gonna put our packages. Alright. So packages are gonna go over there. Obviously, we're gonna add some nice gap for these just to clean this up a little bit. And, again, this is this is fairly rough. Right? If I want to just move this in here, you could see what do we have as far as our packages? Just gonna throw that down there. Alright. So as far as our packages, we've got these shown in a couple tabs. Let's look at ui.nuxt. Do we have tabs here? Yeah, there's like a tabs component. Okay. Tabs equals items. Alright. So let's start flushing this out. We got tabs. We got items equals, let's just write like an inline function. This is another case of I do as I say, not as I do. But let's actually map these packages out. What have we got here? Right? Offer dot packages. We're gonna map these to the actual items. What what shows up in the tab here? The content is x. Does this have, like, a oh, it's got, like, a slot that you can use. I to me, this feels, like, really kinda heavy handed, but, yeah. Whatever. Let's roll with it. Packages dot mat. Actually, let's not. Let's just like flesh these out real quick. Divv4. We use, like, a selected package. Selected package. That'll be what's offer dot packages offer.value.packages. And we'll do the first one. Alright. So that's gonna be our selected package dot ID. Let's call it selected package ID. Good enough. Alright. And then we're just gonna loop through these packages, offer dot packages, offer in offer dot packages. The key here would be key equals offer or, actually, no. We want package Package dot ID. Okay. How are we doing on time? We'll check this. We have got 20 minutes left. Package dot ID. Alright. And then within the package, we are going to have the package. Name, package dot name. Let's just see what this gives us. Then we have a list of deliverables. V 4 package alright. Deliverable item in package dot deliverables. Key equals we'll just call that the item, and we'll do item dot item. K. Item dotitem. K. What else do we have? We have a short description for that. That'd be a p tag. Package dot description. Alright. And, oh, transform failed with, like, 5 things. We've got just too much going on there. There's our packages. Those are not showing. Offer in packages. Package in offer dot packages. Key equals package dot ID. Offer package, oh, package underscore name, package description. Oh, we can't use package. That doesn't make a ton of sense, but, it does make sense. Pkg offer dot packages. Should learn that a long time ago. Item in pkg. Cannot read properties of dot packages, null. Offer dot value. Okay. Alright. So there's our different packages. We probably got a price for that as well. Let's take a look. Cool. I was in a div.flex p dollar sign pkg.price199. Okay. Package dot. It's actually package name. Okay. How are we doing on time? Probably not good. And then we'll just justify between. Okay. And what else? So we got our packages. Let's wrap this. Actually, we wanna do this. We'll do wrap it in a template tag. V if, we get selected packages, selected package ID, offer dot value, unref, offer packages, 0 dot idvif, what, selected package ID equals package.id. I can't read no packages. So this doesn't exist yet. Select the package ID. Offer dot packages. What are we gonna do with that? Let's just leave that. Null for now. That'll get us back to rendering, but we don't really render anything. And we do if offer what's that gonna do for us? Can't read, undefined, offer dot packages. Oh, did I even have that correct to begin with? Unref offer dot packages dot zero dot ID dotvalue.packages.id. Okay. Anyway Alright. And then what we're gonna do, we'll just add a list of buttons here. Div. Flex those. Add some gap to alright. So we'll do buttonv4, package and offer dot packages. And package dot package name. Alright. So we got those 2. And on the click of the button, that click, we'll do selected package ID equals pkg.id. No. Why is it doing that? Let's see. Expanding this when I don't want it to. Thank you, GitHub Copilot. Alright. So there we go. We can switch between these two packages. And then for each package, maybe we just have a buy button for that as well. Let's see what this looks like inside, Fiverr. They kinda send us out where you can order the quantity, you can change, you can add on, things like that. Not super concerned with that. I'm actually going to just totally cheat here. We'll open up my old faithful Stripe account, log in, Go into test mode for this. Just create a new product. We'll call this the theming package, theming offer, direct to themes, buy Bry. Great. We've got more pricing options. This is gonna be a one off. We've got flat rate, customer chooses price, price by package, or bundle of packages. Let's do a flat rate. Hit next. What did we charge for this? 199 for the one package. Do set. Add another price. This will be the 299 for the premium package. Great. Billing period. That's a flat rate. Great. Alright. So we got 2 potential options there. We'll add this product. Of course, I gotta log in. Alright. We're cooking. We're cooking. We got 15 minutes left. And now direct this theme is by Bry. You know, normally, like, taking the time to go through this, I would certainly go in and then, just basically, like, have this created through a Stripe checkout. So we got a payment link that we'll create for this. Great. We'll create this link, copy this, and if we go into our direct assistance where are you, mister directus instance? Admin@example.com. Password. Okay. Alright. So for each one of these packages, offered packages, let's have a buy link. That'll just be a string. We'll serve that on the front end inside a button component. That looks good. Alright. Offer packages. So let's set this up. Right? This is our offer. Where's our packages? Here's the 199 package. There's the buy link for that. Lost my Stripe payment link. We'll create a let's create another payment link real quick. Where are you? Directus themes by Bri. We need a payment link for this one. Create the link. Great. Copy that. Again, this is the quick and dirty way of doing this, but sometimes you got to. Alright. This will be 299 for this specific package. Save. Save. Save. Save. If we look local host, now we should have access to that data. So we can see those prices there. And then inside each one of these packages, after our deliverables, we'll just add a button. We'll add a 2. This is gonna be, package pkg.buyurl. Is that what I set that up as? By link. By underscore link. By pkgpackage_name. And by premium, this should redirect us. Right? Redirect us to direct us. Yeah. Nothing fancy here. Alright. So if I hit now, I hit buy premium. We've got what? 13 minutes left? Buy premium. I go in, I check out, that's great, we're gonna have to fill out this information. But one thing I wanna do here afterwards, right, is after they pay for this, we need to send them to a form. Now if I'm using the Stripe API endpoint, I can set up a referral URL after I check out to redirect. Not going to do that here because I don't really have time to set up all those specific endpoints. Again, we're gonna go quick and dirty mode. We'll go to webhooks. Let's set up a webhook for this. Of course, this is gonna be in my local environment as well. Okay. I gonna cut this close. Right? We'll go into flows. Let's create a new flow. We'll call it catch stripe. Webhook. Catch stripe will trigger on a webhook. Incoming webhook, it will be a post request. What are we going to do when we catch that? Are are am I I I'm I'm getting way ahead of myself. Like, we could catch this and do an email or something like that, but let's just say we yeah. It let's let's do something more interesting. And, switching gears here. Let's just assume gonna make a bunch of assumptions here. I'm just gonna go ahead and check out in this process. If we go back to Stripe where are you, Stripe? I think within the actual payment links, can we do this by, let's say for the premium link, after payment we are going to just redirect. We're gonna go to http local host 3,000/submit/offer/submit. Offers slash submit. Alright. So we're just gonna create a new page. This will be submit dot view. We'll take this information. Submit dot view. And what are we gonna do with this? Right? We're gonna make sure we still let's let's get our offer just to show that up at the top. And then we're gonna build a form to actually submit some data or actually create an order. Again, normally all this would be via webhook and either flows or like a specific endpoint, but this is what we'll do just to throw something out with our remaining 10 minutes. Right? So we've got our freelancer profile. We've got an offer page that customers can buy. So we check the box on that functionality. Technically, I guess this is a win. Is it, a smooth win? I don't know. And then we just want to, like, submit an order form. Alright. So how do we do that? We will do create item. We're gonna need read item. We still wanna pick up that offer. And let's make sure this submit isn't gonna actually work on our front end. Got too many links going on. Local host 3,000 slash offers slash submit. Cannot read properties. Offer oh, I don't have the route params. So let's drop that. Let's move offers underscore ID. We'll make that a folder. This is gonna be dropped into that folder. So it's submit. And this is gonna be index. Alright. So that adjusts things a little bit. And now offers route params.id. Oh, becomes let's go back to the index page. We go here now. Actually, that's gonna be broken. And it no. That won't be broken. Shouldn't be broken. There we go. Alright. So now I've got that, and then we'll just have a submit page. There we go. We've got that. Let's add a h two tag. Thanks for your purchase of this package. Great. And then we've got, like, a form. Alright. So u form, and what are we gonna do with this form? We are, what, we're going to submit this, we're going to need a function here, async function. Submit order. K. We are going to wrap that in a try catch. We'll do constant order equals await, direct us create offers, create orders, create item orders. We have the package, selected package dot ID dot value. Let's not worry about the package at this moment. Then we've got form data, which is just gonna be a JSON object. What else do we have inside this Directus instance that keeps magically getting logged out? 7 minutes, offers, orders. Alright. So we had the user. We had the amount. That's gonna be the selected package. This is probably actually backwards. Right? Maybe we should have submitted this form first and then redirected to checkout after they submitted this form. But, hey. Nobody's perfect. Alright. So form data. What do we want inside the form data? We don't really have to worry about that at this moment. Then we'll just add one more. There's our submit order function. Will this create an order? We can console. Log the order after we create it. But let's just build a quick form that will actually submit this. Alright. So we got you form group. What do we need to actually build a theme for someone. So we need the website URL. We'll have a input for that. V model equals form dot website URL. Actually, that'll be form under underscore data. I actually need a form here. Form, let's make this reactive. And, honestly, here, this will be form. Alright. So form dot website URL. Great. Make that required. And I think that actually goes here on the form group. Again, just rolling off of the Nuxt setup. Label, what else do we want? Form. I got like 35 things going wrong here. Theme, colors. Maybe we add, like, some kind of extra helper text within this. What is the color palette for this theme. Okay. Let's see what this actually looks like. Okay. So we got like a we've got an actual form rolling here. Add some spacing. Add a submit button, new button. Submit form. Okay. Also, let's make sure that you have to be logged in for this. So we'll do constant user equals use state user v if user u form v else. Oh, no. I don't need this. Div v else. Please log in to submit the form. We'll do u button. Ah, boy. Actually, it would be just 2/login. No. It's auth/login. Log in. Alright. Are we actually logged in? I guess I am logged in because I'm sharing a session token here. But if I were to like, go here and look at this, right, it's not gonna show me that because I'm not logged in. And then I could log in, and then I could go back and potentially see that offer or submit. Right. So now that I'm logged in, I can see this information. But we are running dangerously low on time. Let's make this a text area. And let's just run through this. Right? We have purchased something via the offer page. We're now submitting this data. What a mess this one's turned into. But we'll do the directis. Io, violet, white, shades of pink, slight gray. Cool. We hit submit form. What happens what happens if I hit submit form? Nothing happens, because I didn't tie that to any actual button. At submit equals what submit order. Submit order. Submit order. I should be seeing a fetch request. At button oh, it's a click handler. At submit would have been on the form itself. We'll save. Website URL, httpdirectus.io. Okay. Do we wanna track, like, a success state? Success equals ref false. I love cutting this down to the wire. Right? Console log order, Success value equals true. And then if the and no success. Maybe we want to show a success message. The if success. We do yes. Okay. What are we looking at here? Why is it not logged in? V else if v else if. V else. So I fix it. Okay. HTTPS, 1 minute 25 seconds. What happens? Nothing is happening. What did I do? What happened to my click handler? Disappeared. Submit order. I got the wrong I put it in the wrong spot. Okay. Let's try it again. Refresh. Httpdirectus.io. Where is my success with 48 seconds left to spare. Has this actually gone through? Has it submitted an order? Can we actually look at it? Do we have an order inside the system? We do. We don't have the offer and the package in there. So the offer equals the value dot author dot ID. If we go back and we check that just one more time, Submit another order to this. Will that now have the there's the offer. Again, like, we could have this all set up and routed correctly with enough time, but what a wild ride that was to try and get this information actually into the system. So wow. Where would we take this from here? Right? I would go through and set up this logic to be very robust. Again, as far as the offers here, I would probably I don't know that I would create products inside the Stripe account for each one of these offers. I would just set up, a checkout to Stripe that would take the order value or the order amount from a cart, throw that to the Stripe checkout, they would check out. Stripe sends a webhook. We also redirect them back to a page where they can fill out the necessary information, on a page like this to give us the order information that we need, to actually complete that order. So we can see that information here. But that's where I would go from here. Obviously, I would do a little a few things different. If we had to add more than an hour, probably 2 hours here, we could have had a really nice application. But bare bones, this is pretty good. That's it for this episode of 100 apps, 100 hours. I'll catch you on the next one. Thanks for following along.","b22832b2-eb0b-46b5-9a16-84daed24723d",[485],"75b51457-11d9-4c01-b003-11ae478ae94c",[],{"id":157,"number":158,"show":122,"year":159,"episodes":488},[161,162,163,164,165,166,167,168,169,170],{"id":167,"slug":490,"vimeo_id":491,"description":492,"tile":493,"length":192,"resources":8,"people":8,"episode_number":305,"published":494,"title":495,"video_transcript_html":496,"video_transcript_text":497,"content":8,"seo":498,"status":130,"episode_people":499,"recommendations":501,"season":502},"databox-clone","951271332","It’s time to act on your data. So join Bryant as he sprints to build a KPI dashboard to better understand your data. He dives into metrics vs events and pulling in data from third party systems.","42781675-cf4d-4cc4-9460-e0f36c4f5829","2024-05-31","Mission: Databox Clone","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps, 100 hours. I'm your host Brian Gillespie, developer advocate here at Directus. And if you're new to the show, what do we do? We rebuild or build some of your favorite apps, ideas, suggestions in 1 hour less or publicly fail trying.\u003C/p>\u003Cp>Sometimes spectacularly fail, but hopefully not in this episode. What are we gonna be building? Cover that in a moment. There are 2 rules if you're new to the show. 1st and foremost, there are only 60 minutes to plan and build.\u003C/p>\u003Cp>No more, no less. Get what you get. You don't throw a fit, as I like to say to my kids. Rule number 2 is you use whatever you have at your disposal, whether this is AI, GitHub Copilot, Tailwind, CSS, UI libraries, even past projects. Right?\u003C/p>\u003Cp>Whatever we need to get the job done. So back to what we're building, a Data Box clone. So you may have heard of Data Box, you may not have. This was a suggestion from one of my colleagues. Data Box is an well, it's built as an easy to use analytics platform for growing businesses.\u003C/p>\u003Cp>Basically, what I see here is a dashboard. We've got multiple sources of data for that dashboard. I see them speak of a a centralized source of truth, which is one of the strengths of Directus, at least in my mind. We get to be the the hub for all of your data, that you get APIs to work with. So that's why I like this type of project for Directus.\u003C/p>\u003Cp>The bind line here, I'm just calling it KPI dashboard type thingy. Right? We're gonna ingest some data. We're going to display it on a dashboard, report on it, make that whole process easier. Sounds great.\u003C/p>\u003Cp>Let's dive right in. 60 minutes on the clock, and away we go. So looking at the Databox website, right, we've got these different sources of incoming data. We're reporting on those in, like, a time series or just a a metric, maybe a percentage, it looks like. But one of the first things I'd like to do is just study the documentation or the API references for a service.\u003C/p>\u003Cp>So I'm just gonna search for the Databox API. Looks like we've got that here. If I zoom in, we'll just kinda browse through this. Right? So in my mind, it created a couple of similar things in the past where we're storing events and metrics.\u003C/p>\u003Cp>We've even got some of this metric functionality inside our our own dashboard that we use internally for things like doc feedback. But in this particular case, I've not really built like a KPI dashboard. So let's take a look. We've got a see a token in the the data box website, that is the unique identifier that points to a storage container within their warehouse. It's like a bucket for your data.\u003C/p>\u003Cp>Looks like that's all that maybe, like, the different services. I see the metric here. Alright. So the metric is a quantitative measure of performance. That's a pretty good definition of that.\u003C/p>\u003Cp>Basically, all metrics are going to be storing numerical values. Alright. That's a a key part of this. They define that, that metric has a key and a display name, and then we have some data. So here's how we send the data to DataBox.\u003C/p>\u003Cp>Looks like we're referencing that key there, and then we give it a value. And what else? Where is the date information coming from? By default, the current date and time will be used to store information about the events. So when you send an event, it will automatically store the current time, unless you maybe change that.\u003C/p>\u003Cp>Got it. Okay. So at a high level, I understand, kind of, what they're doing here, just building a nice UI on that. On our side for the setup here, I've just got a blank Directus project. Nothing in here, no dashboards, any of that.\u003C/p>\u003Cp>We'll we'll certainly dive into it. But for now, let's dive into Figma and actually sketch this thing out. Right? One of the the big things that come up in my mind is the difference between metrics and something like events. Right?\u003C/p>\u003Cp>So metrics are like an aggregation. Could be of of, like, potential events or other data. Right? So a metric is typically on, like, well, I I won't say typically. There's all all types of metrics.\u003C/p>\u003Cp>You know, on, like, a a website or a server, you might have, like, time to first byte, API response time that you're tracking as a metric, and maybe you wanna report on that every few seconds. Right? On things like billing data, you know, you may have, like, let's say, AAR, our MRR, new users, monthly users, etcetera, you know, something like that. On the CRM side, you probably got pipeline, number of new deals, qualified leads coming in, that you're tracking. So those would all be, examples of metrics.\u003C/p>\u003Cp>And then the events are the actual things that happen, not on the schedule, I guess. On the schedule. Things that happened, basically. Let's call it that. Right?\u003C/p>\u003Cp>This could be when a new user signs up for a service that have happened. Things that have happened. Great. Alright. So in in a system like this, it might be helpful to have both of those.\u003C/p>\u003Cp>Right? Because one of our metrics could be aggregating all the events that happened for that particular day. Page views, you know, video views for Directus TV. Lots of lots of opportunity there. And then, what else are we gonna have on this?\u003C/p>\u003Cp>Right? Data sources, sources of data. Data sources, this would be, you know, our CRM, our, websites, analytics, billing, accounting, etcetera. Whatever those sources are, it looks like Databox has a lot of those built in. You know, we'll probably be just basically creating our own at this point.\u003C/p>\u003Cp>But this feels pretty good as far as functionality is concerned. I think there's a there's a pattern that I've used in a previous project. It's It's actually our directus dot pizzademo. A really cool demo if you wanna check it out. Just a a look at, like, a a full fledged direct to this project in in many different facets.\u003C/p>\u003Cp>There's 5 or 6 different use cases slammed into this thing. But we've got, this pattern of, like, metrics where I've got, my name and description, and I've got, I like some prefixes and suffixes that I could show inside the UI or on a front end, and then I have my data. So instead of shoving it all in one table, kinda separate that out. I kinda like that pattern, so let's do that. Do metric underscore data.\u003C/p>\u003Cp>Looks good. Alright. I'm just gonna pull this up to the side. Again, this is the use whatever you have at your disposal part. Right?\u003C/p>\u003Cp>So I've got my Clean Directus project over here. We're gonna create a couple of new tables. Let's do metrics first. So that's the one we're working on here. Do we need a status for this?\u003C/p>\u003Cp>Probably not. Do I wanna know when it was created, who it was created by? Yeah. Maybe. Maybe we wanna add a sort for it.\u003C/p>\u003Cp>No big deal. Alright. Then we're gonna give this a name. Great. We can give it a, like, a key if we want, as well.\u003C/p>\u003Cp>So if I want to have a unique identifier beyond like a UID, You can also, like, set these up to be manually generated strings for the primary key. Obviously, you've gotta enter that every time, but I can go in here and make this string unique. And if we're looking for a key, you know, we might want to make that URL safe and just use the input option to slugify it. Alright. And then we've got, like, a description of the metric.\u003C/p>\u003Cp>Maybe I'll go in and add a note for this. What does this metric measure in detail? Great. And I've got prefix and suffix on here. Not sure we necessarily need those.\u003C/p>\u003Cp>We'll just keep that off for now. But basically, we're gonna make sure we unhide this ID. Need this ID when sending data, sending metric data. That'll be the collection that we create next. Right?\u003C/p>\u003Cp>So we've got a name, we've got a key, we've got a description. Right? We've got API response time over here just as a a reference, but, what am I looking at initially? Let's call this, like, ARR. Alright.\u003C/p>\u003Cp>Tracks changes to annual recurring revenue over time. Great. Alright, so we got a metric in there, looking great. Now we need a place to store that metric data. Right?\u003C/p>\u003Cp>So applying that same pattern, I'm just gonna fade that away. Just close this out. Alright. We're gonna have a metric data table. Metrics, data, metric data could go either way.\u003C/p>\u003Cp>Naming stuff, always the hardest part of these episodes. Right? So on this one, we've got, like, the optional fields here, just shortcuts for things like recording a timestamp whenever a thing is created. Now one of the things that I like to do on something like this is just remove any ambiguity and call it a time stamp, or create it at, but you can change those on the fly as well. So I'm gonna unhide that.\u003C/p>\u003Cp>For our metric data, we're also gonna have a value for that data. Right? This is gonna be let's go with a decimal on this, just because we might wanna store something with the decimal place. It's not always integers. Great.\u003C/p>\u003Cp>We'll choose type decimal. And if I wanted to flesh this out even further, I could go to the advanced settings and, like, control my precision and scale if I needed to. If I go into the display here, I can also auto format this, which Directus will try to guess as to what format this should be in and apply some styling and, conditional formatting for you, make that look nice. Alright. So we got a timestamp.\u003C/p>\u003Cp>We've got metric data. Let's link these things together with a many to one relationship. So Directus makes it super simple. To create these relationships, we're just gonna call the key for this field inside metric data. We're gonna call that metric, and then we have our metrics over here.\u003C/p>\u003Cp>So that's the related collection. Now I could hit save here, but I'm gonna open up in the advanced settings and I'm gonna go to the corresponding field under the relationship tab. And here I can go ahead and create that inverse relationship in the metrics table. So I'm just gonna call this data just to keep it simple and Directus will show me, hey, we're gonna create this field for you. So if I delete a metric, you know, maybe we do wanna delete the metric data just because it doesn't make so much sense without the identifier and, you know, the name and the description of that metric.\u003C/p>\u003Cp>Alright. So this looks pretty good. We'll just, make that half with clean this up a little bit. Right? What else do we have?\u003C/p>\u003Cp>We have events incoming as well. Right? So, again, the difference between events and metrics, like, a a metric is something that is going to be at a specific point in time. An event is something that that happened. Right?\u003C/p>\u003Cp>So a a ARR doesn't happen. Right? A user subscribes and that has an effect on ARR. So also starting to sound like a pilot or a pirate in this episode, not a pilot. So we got an event.\u003C/p>\u003Cp>Let's see. We'll, again, do the same thing with the timestamp. The events are probably not going to to change much, so maybe we'll just call, like, created by, like, the user here. That's cool. Alright.\u003C/p>\u003Cp>And for the event, let's see. What are we gonna have on the event? We'll probably have something like we have on, we'll have a name for the event. Maybe that's just a string. We could have a key for that event.\u003C/p>\u003Cp>Key. What kind of event key is this? We will go back in, make sure this is slugified, make it URL safe. We got the user. We probably wanna show that in this case.\u003C/p>\u003Cp>Even though those are hidden by default, we wanna show the timestamp of this event. And then, you know, you might have, like, a a JSON field to pass metadata. So in this case, I'm just gonna pick the code interface, choose the JSON type, and we'll just call it metadata in case I wanna store additional stuff on the events. Alright. That's looking nice.\u003C/p>\u003Cp>And then, you know, to bring this all home, let's add some icons. But, one of the other things that I'm gonna do is add the sources just in case we could we wanted multiples. Like, I'm imagining if you got your CRM data, you're probably looking at 2 or 3 different metrics within that. Or, like, if you've got your Stripe data, you're probably checking your ARR, your MRR, number of new sign ups, your churn rate, things like that. So, there's maybe one more level of abstraction here that we're gonna go for.\u003C/p>\u003Cp>Alright. So we've got events. Let's just get chip extraction. That looks good. Metric data, we could probably hide that if we want to, or we just call it data, see what we got.\u003C/p>\u003Cp>There we go. Data usage. Good enough for me. Alright. Great.\u003C/p>\u003Cp>So let's add that sources, or this could be something like services. Maybe that makes more sense in this particular case. We'll use the UID. You know, we'll see when users update these services. We'll give it a name, and then we can link, a service to, you know, both of these, like events and metrics.\u003C/p>\u003Cp>So we'll link, use the mini took one here. We'll call this the service that is related to this metric. And if I flip over to that advanced section, again, I could go in and add the different metrics that are associated with that service. Great. And if we go to events, same thing.\u003C/p>\u003Cp>Right? I might call this service Services. K. Go to our relationship. We'll create that extra corresponding field.\u003C/p>\u003Cp>It show the related values in the display template. And now we've kind of got this whole thing fleshed out. Right? We've got ARR, the service here in this case. And maybe this is Stripe as the service.\u003C/p>\u003Cp>Great. Okay. Looking great. Love it. Love it.\u003C/p>\u003Cp>You may we do some drag and drop to kinda change the stuff around. See the ID. Don't really need it. But let's go in and actually, you know, populate some of this data now that we've we've got something in here. Right?\u003C/p>\u003Cp>So I'm gonna go in and create something here, but you could see I can't change my time stamp, so I could manually change that. Let's see. The metric we're using here is ARR, so this is gonna record the current value because of the way that time stamp field is set up, or the current date and time. I'm sorry. We'll do something like $1,998.\u003C/p>\u003Cp>That's that's what we're making ARR right now in this particular app. Not doing so well, but, if I'm manually throwing these in here, I can't edit this. So two ways I could populate this data. I could use the API to quickly, like, shove data in here for this specific metric if I wanted to. That's available to me.\u003C/p>\u003Cp>Right? Or I could, you know, remove the restriction here on this to basically let me edit that value if needed. And then you could see on create, there's kind of this save current date and time thing going on as well. So I might do both here just in case we need to edit that. I'll change that.\u003C/p>\u003Cp>Save it. Cool. Alright. And then, you know, if we change this again, we go to change it to 8:8:8, maybe last Thursday. That's what our ARR was.\u003C/p>\u003Cp>And you can see that time stamp was populated, so I could turn off that behavior. But if I just hit the save button twice here, we'll get some actual data from this. But if I wanted to do that via the API, that's all ready to go for me as well. The only thing I need to do is either use a static token or make that publicly available. Right?\u003C/p>\u003Cp>In this case, I might just create a static token. So I'm gonna go into my user directory. We'll say, scroll down to the bottom here. We're gonna create a static token to use as we're calling this. And I'm just gonna pull up a little app that I've got called Bruno.\u003C/p>\u003Cp>It's kinda like Postman, except it is offline first. So Postman, you have to be logged into their service. Bruno is, I think it's open source and offline first, basically. Like, it sorts all of your stuff locally. So, what is the name of this?\u003C/p>\u003Cp>This is metric data. Right? And we've got our metric that we're using here. So I could copy this. And And as far as the payload, we should just need to pass, like, a timestamp and a value.\u003C/p>\u003Cp>So this is using the, timestamp value. So we've actually got the time zone in here as well. But if we go in, we're just gonna do 8055 post metric data. Let's set this up as a post request. My headers, I've got, like, a Directus token set up already for this.\u003C/p>\u003Cp>I'm just gonna replace that value. Is it secret? Secret for now. There we go. We'll save.\u003C/p>\u003Cp>Changes saved successfully. And let's actually just see if we can get the Directus data first. Says my token is incorrect. Correctamundo. That's not great.\u003C/p>\u003Cp>Let's try this again. Did I actually save the user? Maybe I saved the wrong user. I think there's only one user, but alright. Delete.\u003C/p>\u003Cp>Save. There's the value. I'll hit save. Try this one again. There we go.\u003C/p>\u003Cp>So now we can see our actual metrics. Right? That's the beauty of Directus. Take any SQL database, wrap it, gonna sit alongside of it and give you APIs in order to access it. But now I could go in and I should be able to post this with just like a raw JSON body.\u003C/p>\u003Cp>Got value of, what, 777. The metric, we could just copy that. Right? That's the unique identifier for that metric. I could have set that up to be, again, a manually entered string.\u003C/p>\u003Cp>That might have been easier for this. And then we're gonna add a time stamp to this. So if I populate the time stamp, it should do that for me. Let's just dig back in time here and do that. So we'll send that one.\u003C/p>\u003Cp>Now if I refresh, it should have a couple of metrics, 777 generated less than a minute ago. Right? So it's not saving that timestamp. Maybe we just need to actually turn that off for now. So we go into our metric data.\u003C/p>\u003Cp>Just turn this off. Where are you? Save current on create, do nothing. Alright. So now if we try this again, you can see that the date that's being stored is that 22nd that we had.\u003C/p>\u003Cp>Just mix this up a little bit. 555. Dig back into time. Great. Alright.\u003C/p>\u003Cp>Sweetness. Okay. So now we got some data. Right? Let's build a dashboard out of this data.\u003C/p>\u003Cp>So we're gonna go into the insights module inside Directus. The beauty of insights is that I can build dashboards with low code, no code. Really, it's no code unless you start needing to adjust some of the the JSON within the filters, and use some dynamic variables. But beyond that, we could create these dashboards really easily. Right?\u003C/p>\u003Cp>So we'll call this the our dashboard, and we've got a do we have an no. Let's do it like a chart. Chart. See what we got. Yep.\u003C/p>\u003Cp>There we go. Got a chart going on. Let's start adding to this. Right? So if I look if we do I have the data box up?\u003C/p>\u003Cp>Let's just see what they've got. Like, first on the list here is, like, revenue. I see new customers, like, website sessions, all pretty standard stuff. We're we're just gonna go in and let's just show, like, a a metric. Right?\u003C/p>\u003Cp>Like, what is our current ARR? If I wanna send this out to, you know, one of my team leaders or, a CEO or, you know, my manager, like, how how are we gonna set this up? So in this case, the collection that we're working with is the actual metric data. That's where we're storing the values that we wanna see. The field that we wanna report on is the value itself.\u003C/p>\u003Cp>And the aggregate function here is basically, like, what are we gonna run to get this specific data? In this case, you know, I really just want, like, the last value. As of, like, the last value in that series, what's our ARR currently? So I could do this, and the sort field here would probably be timestamp. And then the filter, basically, like, of all the metric data items in there, how do we select just the ones for ARR?\u003C/p>\u003Cp>So I'm gonna go in. We've got our metric thing here. You know, I could go by the key here, that the key is equal to arr. Hopefully, I set that up, we'll find out in a moment. And then I'm just gonna scroll down.\u003C/p>\u003Cp>We'll give this a name, like current ARR. Great. And we could see that. So we got 777. That's the last value in that series.\u003C/p>\u003Cp>Right? Maybe we add a prefix, so we get dollar sign. But let's just check that. And if we look here, 4 minutes ago, we have 777. Now if I were to go in and delete that value, right, that should become 999 when we go to the dashboard.\u003C/p>\u003Cp>Okay. All is right with the world. That's that's looking nice. So this is a little underwhelming though. Right?\u003C/p>\u003Cp>We wanna see more data than this. So actually, let's let's add some time series data to this. Let's track those ARR changes over time. So the time series is good for this. You could also use the line chart, that will allow you to get multiple groups.\u003C/p>\u003Cp>So if you wanted to, kinda look at groups of users within a a certain time period over, like, a certain set of events, that sort of thing. So, again, we're going to choose Metric Data. The aggregation here, if I've got 2 values for a given day. Right? This is probably the way that I'm gonna report on this, is by the by a given day.\u003C/p>\u003Cp>Do I average those values together, or do I do the minimum or the maximum? Let's just average them together. You know, one of the things that we might set up is just a a flow or an automation to go in and run those metrics for us on a daily basis. For the group precision, we'll just use day for this. Date field, we got our time stamp.\u003C/p>\u003Cp>And when it comes to the date range, this is, a little tricky in that, like, if I select one of these, it's always gonna be that specific date range unless I do something like automatic based on data. Or, you know, I could fill this in with other and make it like a dynamic variable where I could pick past 30 days, past 14 days, etcetera. In this case, what I'm gonna do, I'm gonna just set that to be automatic based on the data, and the value field here is gonna be the value. Plain and simple. We'll do ARR over time.\u003C/p>\u003Cp>Great. Looking nice. Okay. So now we can actually see those changes. And again, this is a full drag and drop builder.\u003C/p>\u003Cp>One of the nice things here is you could see if I connect these 2 together, the little border radii, border radius, those all they connect nicely. It's very subtle detail, but very helpful. Alright. So now we got a chart. You know, we could make this type of thing dynamic as well.\u003C/p>\u003Cp>It looks like the Databox dashboards are relatively fixed. I see like month to date for a lot of these. But let's say if we wanted to zoom in on a particular timeline. Right? You know, hey.\u003C/p>\u003Cp>This is only data for May, but, you know, maybe I wanted to look at last month's data. So what we can do, you can make these things dynamic using variables, like either the global relational variable, which allows you to select a specific thing. In this case, maybe you wanna report on, a specific metric. Or like a global variable here could be based on time. Right?\u003C/p>\u003Cp>So we could do something like this where we have variable key. We'll do date from, and that'll be a time stamp. Great. Use the date time interface. In the US, We don't use that 24 hour clock very often.\u003C/p>\u003Cp>Alright. So let's call this date from. Alright. So now we get like a panel here. We could duplicate this thing.\u003C/p>\u003Cp>We can call it date 2. So in the variable key, we'll call it date 2. I'll make sure I change my panel header just so I don't confuse any users of this dashboard. But we're all about empowering people. You know, if you've got, like, a ton of internal data, you could potentially and it's in a SQL database.\u003C/p>\u003Cp>You could connect Directus to it, get APIs, and get a dashboard experience out of the box, right, that that all members of your team would be able to use. So now that we have created those relationships or those variables in this case, these are not relational variables, they're just global variables. I can use the names of those inside my other data. Right? So current ARR, we're always looking for that to kind of show the same data.\u003C/p>\u003Cp>Right? What is the current ARR? If we're talking current, it's always x. But in in the case of like over time or like showing a list of records, maybe I want to filter that for a specific date range. So I could do something like this where I say the timestamp, I've got these filters here that you can see.\u003C/p>\u003Cp>But we also have greater than or equal to. And I could do something like this, is between, and I use a mustache syntax. And now we can use those date variables. Okay? So now I'm not seeing anything, right?\u003C/p>\u003Cp>What do I change this to? For default value, we have some dynamic variables here too. So let's say, for the date from, maybe we change it to the start of this year. So just back up to Jan 1. That's great.\u003C/p>\u003Cp>And then for date 2, maybe we always wanted to show, like, the latest value. So we could just go to something like dollar sign now, which should populate the current time stamp in that particular field. If I just refresh, now you could see what we've got going on here. So if I back this up, like let's go to like May 23rd. Okay, so now you could see like the scope of my data changes within the chart.\u003C/p>\u003Cp>That's how I can make these dashboards dynamic. Really, really neat feature. Right? Alright. So going back to our our board here.\u003C/p>\u003Cp>We've got some metrics. We've got metric data. We've got the sources planned out. We've got events. We haven't populated any of those.\u003C/p>\u003Cp>But one of the things that I kinda jumped the gun on here was what do we actually want out of this specific application. Right? We wanna manage all of our metrics in one place. That's right. Directus becomes the single source of truth for all of these particular metrics that we are managing, that we have under our control.\u003C/p>\u003Cp>And what else? So we're gonna build a usable dashboard for these metrics with usable dashboards. Yeah. We could slice this a couple different ways. And then as, a larger goal, you know, maybe we wanna integrate some third party service.\u003C/p>\u003Cp>3rd party service. Alright. So let's take a look at, like, the events side of it. Right? Events would be very helpful, I created a new account.\u003C/p>\u003Cp>Right? So let's just call this 1, created a new account. Alright? So let's just call this one, like, new account registration. Yeah.\u003C/p>\u003Cp>Seems great. New account and registration. I've got metadata here that we could store. It's great. We'll just pick the service.\u003C/p>\u003Cp>You know, maybe that's we create a new one. Can we create a new one from here? We can. Let's just call it website. Great.\u003C/p>\u003Cp>Okay. And cool. So now we got an event. The name is no account registration. It happened at this particular time with this particular user.\u003C/p>\u003Cp>I could go in and maybe I just create, like, a a fake user as well to populate some of these events. Fake user Fake user 2. That's a real original name. Right, Brian? Yeah.\u003C/p>\u003Cp>Okay. Alright. So we got another event. Maybe we want to, in this case, I'm gonna just disables the read only part of this. And oh, I didn't save it, did I?\u003C/p>\u003Cp>Disable Disable, just in case I need to change those items. Alright. And the difference here is we're gonna assign this to fake user. Let me go up to the 3 dots. If you are pretty new to Directus, this is a helpful hint here where you can use these keyboard shortcuts, command s to save and stay.\u003C/p>\u003Cp>Or let's say you wanna create a new record. You could save and create new. In this case, I'm gonna save as a copy. So we're not gonna touch the original event. We're just gonna create a copy of it, and I'm gonna assign this to fake user, account user registration, blah blah blah.\u003C/p>\u003Cp>If I was doing this on my front end, I would certainly be passing, like, some metadata of, yeah, what account let me maybe some of those account details that would be relevant. Of course, not any personally identifiable information. Don't wanna store any of that. And then we change the timestamp, and we change that to May, etcetera. Okay.\u003C/p>\u003Cp>Alright. So I've got a couple events. Right? And now, let's say I needed to run a report that shows me all of the new registrations within a certain time period. Right?\u003C/p>\u003Cp>Now I could go in and have, like, basically set up a a chart based on that. Right? And I could use this event data to get a list of those events or kinda aggregate those. But how would you track the the changes between those things? So, you know, how do I show on a dashboard how many new registrations there were 2 weeks ago versus today?\u003C/p>\u003Cp>Right? Or how can we analyze those things under time and compare them to other details? So in that case, maybe we want to convert those events over into metrics. One way you could do that is with direct as flows. Some automation inside the system.\u003C/p>\u003Cp>Right? So every day, maybe I want to go in and summarize all this information and store it so we can make quick work of analyzing the data. So in this case, let's just set up an automation. Right? We're going to summarize registrations.\u003C/p>\u003Cp>Sounds good. Alright. For an icon, You know, my recovering designer in me always has to have an icon. And for the trigger here, we're gonna trigger this at regular points in time. I always get hung up on the Kron syntax.\u003C/p>\u003Cp>Kron syntax for every minute. Let's see what we come up with for this. Every minute. Okay. You're probably gonna only set this up to run once a day.\u003C/p>\u003Cp>I'm gonna run this once a minute just so we can, like, get a good look at it. And you could even, like, manually trigger this sort of thing if you wanted to. But in this case, we'll set that up. And now what do we wanna do with this? Right?\u003C/p>\u003Cp>So if we're gonna summarize this registration data, we'll go in, and we need to get that registration data. So we'll say get data, and I could go in and hit read data. So we're gonna get the items from the database. Let's use full access, which gives it full permissions instead of the ones from the user, from the trigger, or the public role, in this case, like a cron job. We'll go to our metric data, and we can either specify the specific IDs we wanna find, or we can set up a query.\u003C/p>\u003Cp>So these are just the standard query, like, global query parameters that we support via the Directus API. Just flesh this out. Where are, which metric are we looking for? We want the metric. So here, we're at our metric data.\u003C/p>\u003Cp>I can also dig deeper into the actual related fields. Right? So the related field, metric key equals let's say equal no. Metric key. Yeah.\u003C/p>\u003Cp>I think it would be something like this. We'll see if this actually works. Key equals arr. No. Okay.\u003C/p>\u003Cp>Alright. So we look for the filter. This is a global parameter. So within the metric data, we look at the metric, which normally is just stored a UUID for that. But we can also go into the related fields here and say the key of this metric is equal to ARR.\u003C/p>\u003Cp>Is that actually gonna run? We'll see. Right? So we can see this already ran twice. If I wanted to to sit around for another minute, we can take a look at it, see what happens, or, you know, I could manually trigger this thing as well.\u003C/p>\u003Cp>So let's do that. We'll just go in metric data. Cool. Summarize registrations. Alright.\u003C/p>\u003Cp>I'll hit save. And now if I go into metric data, I can also run this thing. I forgot to turn off the need to select something. But now if I run that, we could see if we actually got the data that we were looking for. Great.\u003C/p>\u003Cp>I can see this is the data that we need. And just to make sure that's working as intended, I'm gonna just add another one here and maybe maybe just leave off a metric to make sure we should have 4 values inside that flow instead of 5. So, again, I'll just run this flow. Great. I'll go back and look at the logs really quickly.\u003C/p>\u003Cp>And I can see I'm only getting 4 values here. That's great. Cool. Alright. So now maybe I want to summarize those.\u003C/p>\u003Cp>But this is not our I'm actually looking at the wrong thing, aren't I? I'm getting the data from our metric data and not our events. So we need to make sure we actually change that up. Just an oversight here on my part. So in this case, we're gonna do the key equals new user registration.\u003C/p>\u003Cp>Alright. Now let's test that again just to make sure this is actually gonna run. K. Having a look. Alright.\u003C/p>\u003Cp>No. Are we getting our data? Did we call it something else in our events? New account registration. Always remember what you name things.\u003C/p>\u003Cp>It's a challenge. All right. Just paste that key here inside our query and hit save. Alright. So, again, maybe we wanna change where we're actually running this from to.\u003C/p>\u003Cp>Go to Events. Alright. Lots of back and forth here. So I'll select all these just because of the way that this flow was set up. Normally, I would set this up to be a cron job like we had before.\u003C/p>\u003Cp>But now if I check the logs, we could see we've got all of our events here. That's great. Now we are going to collect all those events and save them as a metric, right? We could also specify, You might want to use our dynamic filters as well, right? So let's actually just save this first.\u003C/p>\u003Cp>We'll come back to it. So we're gonna format this. Let's just do a quick run script. We'll call this like format data. And if we see here, we've got, this is our actual data.\u003C/p>\u003Cp>All right, so I'm gonna just copy paste this into Versus Code here, so I've got it. And then inside this operation, this run script operation, we can run arbitrary JavaScript to do whatever we want. So in this case, we are just going to actually combine these two pieces of data. Like, summarize how many we just want to get a count of all of the events for this, right? So something like this where we have, we're gonna get the data.\u003C/p>\u003Cp>We call it get data. So we could say that const events equals data dot get data. As each operation within a flow runs, it appends the data that operation returns under this key. So after this step, the results I'd be able to access via data dot format data inside a run script. But in this case, we're just gonna do const events.\u003C/p>\u003Cp>And then maybe we just return the the length of those, I think. Right? Simple as that. We don't have any values that we're like summing up here. We're just going to return events dot length.\u003C/p>\u003Cp>And this could even be simpler. It's basically just return dot length. Right. And actually maybe we do something like this where we say this is the metric data. The value is data dot get data dot length because we're returning an array there.\u003C/p>\u003Cp>The timestamp would be, what, new date. So we'll get the current timestamp. And then the service Okay. Maybe we'll open this up in a new tab. Just duplicate this.\u003C/p>\u003Cp>We'll go in and like our website service, I could pick this off up here. Or if I look in the information, no it's not up there. I'm just gonna use the URL to grab this specific service. Where do we go? Alright.\u003C/p>\u003Cp>So we'll populate that service ID. And then we have the, what, metric. So we're gonna have a metric attached to this, which we have not populated yet. So let's just do that as well in this second tab. We'll go in and create a metric.\u003C/p>\u003Cp>We'll call it new account registration. New account registration. Okay. Save and stay. And then I get my ID for this metric.\u003C/p>\u003Cp>Just paste that in there. Great. So we can kind of see the the format that we want here. And, actually, we don't need the service because that should be on the, the actual metric here. So this is gonna be the metric data.\u003C/p>\u003Cp>And we're just gonna return that value, return metric data. All right, great. And then the last step of this puzzle would just be to go in here and create that data. So we have formatted the data that we want. We're just going to create metric data.\u003C/p>\u003Cp>We'll use the metric data option here. We've got our full access. And for the payload here, I'm just gonna do this, where we say format_data in like a mustache syntax. Doesn't seem to like that. Maybe we wrap it here.\u003C/p>\u003Cp>Okay, all right, so now let's actually try and run this flow to see what we get out of it. All right, so I'm here on my events, Right, we've got 2 events. We should get like a summary for those. So if I go here, now we should have like our Metric Data. Okay.\u003C/p>\u003Cp>50. No, that's not correct. That's the one we manually created. Let's take a look at this and see where things are falling out. The time stamp.\u003C/p>\u003Cp>Oh, we're not getting the proper value for the timestamp. Alright. Let's take it. Do we need to save that to ISO string? I think that's maybe what we need.\u003C/p>\u003Cp>2 ISO string. Is it 2 ISO string? Let's just take a look. 2 ISO string. To, okay, ISO is capitalized.\u003C/p>\u003Cp>I always forget about that. All right. There we go. To ISO string, new date to ISO string. And we might even do it like this, just to new date.\u003C/p>\u003Cp>Date. Not data. To ISO stream. See if that gets us where we want to be. So we'll just go back to our events, run this flow, summarize our registrations, see what we get out of it.\u003C/p>\u003Cp>It's great. Create our metric data. There it is. That's all running, right? So now I could switch this over back to a CronJob.\u003C/p>\u003Cp>Let's call it Cron. Yep. Set this up on an interval. Save it. And, you know, now in the background, this is going to run.\u003C/p>\u003Cp>It's gonna summarize all those events for our registrations. Right? So now I got I have the individual events that we can see and, you know, I can potentially act on those individual things when they come in. You know, maybe I wanna send those to Slack for our sales team to follow-up on. But also on our metrics.\u003C/p>\u003Cp>Right? Within our dashboard, I can see I've got our data for that specific metric as well. And in this case, maybe I wanna clean this up a little bit if I'm looking at it inside the data studio. So I could go into this. We look for our metric inside the metric data.\u003C/p>\u003Cp>Just use a display for this, call it name. Great. And now if I look, I could see, okay, this is the new account registration. I could see the value for that. And again, like, we could make this dashboard totally customizable.\u003C/p>\u003Cp>Right? What if we wanted to change this metric that we're actually showing here? What if I wanted to make this chart variable? So we could call this something like Metric over time. And now inside the collection, before we do that, we're going to create a global relational variable.\u003C/p>\u003Cp>I'm gonna call this key, the metric underscore key, just so I don't get those confused. But we're gonna use the collection metrics here. We're gonna pick a single metric that we wanna use. And we're just gonna say pick a metric to Report on. Great, okay.\u003C/p>\u003Cp>So we'll add this into our lineup. And here, I want to show just items from the metric that I have selected, right? So how do we make that work? I just basically go in here. I'm going to toggle raw data.\u003C/p>\u003Cp>I'm going to use that same mustache syntax. We're going to say metric_ key. Alright. Let's take a look and see if this is actually gonna work out how we want it to. Alright.\u003C/p>\u003Cp>I don't see anything going correctly there. But if I look for new accounts, no. That's not doing what we want. Right? So let's go in and actually fix this.\u003C/p>\u003Cp>This is not the way we wanna do it. Right? We still wanna report on that metric data, but we want to only filter on the metrics that are selected there. So we're gonna do this instead. We're gonna change that to our filter.\u003C/p>\u003Cp>And the metric ID has to be equal to the metric key that we use from that variable. So you can use just regular variables like date or string or something like that if you wanted to. Or you can use relational variables. So the other thing that we might set up here is like an AND filter. This should be AND by default.\u003C/p>\u003Cp>But let's see what we get, right? ARR. This is still not populating correctly. We've broken something. Our date and value fields were not set.\u003C/p>\u003Cp>So let's test this out one more time. We get no data. If I pick a metric, now we can see our ARR And maybe I wanna change the display template on this as well, where we're just showing the actual name of that. There we go. All right.\u003C/p>\u003Cp>So now I can see, hey, we're looking at our ARR over time. Or if I wanna check out our new user registrations, we could see those, just 2 on this particular day. Great. And let's just change this a bit, right? We'll do like 6 previously.\u003C/p>\u003Cp>Let's put that like last Tuesday. Save as a copy. And now if we go back to our dashboard, start to be able to see some of this, right? So we can see the variance there. Great, it's a good way to do it.\u003C/p>\u003Cp>Alright. So same kind of thing. I could potentially call a third party service using Flows if I wanted to. So, you know, there are multiple ways to do this, but I could go out. And if we use Stripe as a good example, I guess.\u003C/p>\u003Cp>Running out of time, so Stripe is gonna have to work. We'll just go to Stripe. I'll log in. I'm going to look at just some of my test mode data. Which account am I in?\u003C/p>\u003Cp>Let's get into one of my test accounts. Boom boom boom boom. Alright. Developer mode. There we go.\u003C/p>\u003Cp>Okay. What are we gonna need from this? Right? We're probably gonna need a key. API key, create a restricted key, key name, test key.\u003C/p>\u003Cp>What do we wanna look at? Invoices. Read invoices. Permissions. Okay.\u003C/p>\u003Cp>Smart enough. All right, reveal test key. I'm going to copy this down. And let's look at Stripe API list invoices. List all invoices.\u003C/p>\u003Cp>Alright. So let's do this. We are going to get today's invoices. Maybe we just wanna show what we brought in on that particular day. I'm gonna, again, trigger this manually, not requiring a selection.\u003C/p>\u003Cp>Great. And then inside Flows, I could just add regular HTTP requests here. So we'll say call Stripe. Now a, like a more in-depth way to do this would be to create, custom extensions for things like this, where, you know, you've got more functionality built into it. But I could just go in and get a list of all the invoices here.\u003C/p>\u003Cp>Let's see what the curl is gonna be for this API. Alright. This is just gonna be a get request. We're gonna use the authorization header. Use bear.\u003C/p>\u003Cp>And I'm just gonna paste my token that I've got there. Alright. Cool. As far as the request body, do we really need anything there? No, I'm not sure.\u003C/p>\u003Cp>Let's just go in and test this out. Right? Get today's invoices. Go back to flows. We'll see what ran did we get some actual data.\u003C/p>\u003Cp>Status, okay. We get a list of the data coming back. And cool, all right. So what I'm going to do, I'm going to just pull this data into, again, Versus Code. And that just helps me traverse it easier when I'm putting this together.\u003C/p>\u003Cp>Alright. Alright. So today's invoices, you know, let's go in and make a new metric for this. Today's payments, today's invoices, invoices today, daily. That's great.\u003C/p>\u003Cp>Whatever we wanna call it. We'll add our Stripe service to this, just so we know that's linked to Stripe. We'll hit save. Copy this metric ID. Cool.\u003C/p>\u003Cp>And a couple of things, right? What are we going to do? We're going to run a script, calculate totals. Okay. How we doing on time?\u003C/p>\u003Cp>Alright. Let's cheat. GPT, you can type faster than I can. Write a JS function to summarize, to calculate the invoice total for this data, the total of all invoices. Total of all invoices.\u003C/p>\u003Cp>There we go. So this is gonna spit out a function. It's gonna be a reduce function, but hopefully, it figures out the exact structure of the data. Give it a shot. Right?\u003C/p>\u003Cp>Alright. So we'll just paste this here. This looks to be correct. Invoice data dot data. Let's just traverse it ourselves.\u003C/p>\u003Cp>Right? InvoiceData. Data. Amount due. Okay.\u003C/p>\u003Cp>Calculate total invoices. Let's do amount paid. Paided. Amount underscore paid. Okay.\u003C/p>\u003Cp>And then we're gonna call this function. Alright. Return, Let's say totals equals calculate total invoices. And the data here is gonna be data dot, what do we call that? Call Stripe.\u003C/p>\u003Cp>Data dot call Stripe. How are you on time? We're running dangerously low on time. Call Stripe dot data? Call Stripe dot data dot data dot zero?\u003C/p>\u003Cp>Is that what it's looking for? The InvoiceData, InvoiceData.data. Oh, it's looking for that. Okay. All right.\u003C/p>\u003Cp>That's what we'll do. We'll return payload to update this. So, oh, forgot to close that off. Okay. We're going to return, what, a symmetric.\u003C/p>\u003Cp>Okay. What's gonna be our metric? There's our there's our North Star metric. There's the ID for that. I think I'd already copied that before.\u003C/p>\u003Cp>Okay. Then we've got the value equals the totals and the timestamp equals new date to ISO string. Alright. Can we actually get this first shot? I don't know if we will or not, but we'll try it out.\u003C/p>\u003Cp>Create Metric. Okay. Calculate totals. That's what we're going to stuff here in our payload. And we will run this thing on metric data.\u003C/p>\u003Cp>Full access. How we doing? Time, 42 seconds left. We are chugging along. Alright.\u003C/p>\u003Cp>Get today's invoices. Did this actually create our metric for us? Yes. 30,000. There it is.\u003C/p>\u003Cp>Bada bing bada boom. If we go into our dashboard really quickly, we should even be able to report on this, today's invoices. We could see that value of $30,000 even though it's not inside the well, we don't have any other data points, so there's not, like, a a necessarily a chart here to show. But within 10 seconds left, bada bing bada boom, we're calling that a win. We have managed all of our metrics.\u003C/p>\u003Cp>We have build usable dashboards or built usable dashboards, and we have integrated Stripe as a third party service to pull in our KPI dashboard. Right? This is a little underwhelming here, as far as, like, the actual dashboard. That would be pretty easy to flesh out, though, as soon as I started populating all of my data. So for me, that would be my next steps.\u003C/p>\u003Cp>I've hoped you'd enjoyed this episode. Hope to see you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps, 100 hours. I'm your host Brian Gillespie, developer advocate here at Directus. And if you're new to the show, what do we do? We rebuild or build some of your favorite apps, ideas, suggestions in 1 hour less or publicly fail trying. Sometimes spectacularly fail, but hopefully not in this episode. What are we gonna be building? Cover that in a moment. There are 2 rules if you're new to the show. 1st and foremost, there are only 60 minutes to plan and build. No more, no less. Get what you get. You don't throw a fit, as I like to say to my kids. Rule number 2 is you use whatever you have at your disposal, whether this is AI, GitHub Copilot, Tailwind, CSS, UI libraries, even past projects. Right? Whatever we need to get the job done. So back to what we're building, a Data Box clone. So you may have heard of Data Box, you may not have. This was a suggestion from one of my colleagues. Data Box is an well, it's built as an easy to use analytics platform for growing businesses. Basically, what I see here is a dashboard. We've got multiple sources of data for that dashboard. I see them speak of a a centralized source of truth, which is one of the strengths of Directus, at least in my mind. We get to be the the hub for all of your data, that you get APIs to work with. So that's why I like this type of project for Directus. The bind line here, I'm just calling it KPI dashboard type thingy. Right? We're gonna ingest some data. We're going to display it on a dashboard, report on it, make that whole process easier. Sounds great. Let's dive right in. 60 minutes on the clock, and away we go. So looking at the Databox website, right, we've got these different sources of incoming data. We're reporting on those in, like, a time series or just a a metric, maybe a percentage, it looks like. But one of the first things I'd like to do is just study the documentation or the API references for a service. So I'm just gonna search for the Databox API. Looks like we've got that here. If I zoom in, we'll just kinda browse through this. Right? So in my mind, it created a couple of similar things in the past where we're storing events and metrics. We've even got some of this metric functionality inside our our own dashboard that we use internally for things like doc feedback. But in this particular case, I've not really built like a KPI dashboard. So let's take a look. We've got a see a token in the the data box website, that is the unique identifier that points to a storage container within their warehouse. It's like a bucket for your data. Looks like that's all that maybe, like, the different services. I see the metric here. Alright. So the metric is a quantitative measure of performance. That's a pretty good definition of that. Basically, all metrics are going to be storing numerical values. Alright. That's a a key part of this. They define that, that metric has a key and a display name, and then we have some data. So here's how we send the data to DataBox. Looks like we're referencing that key there, and then we give it a value. And what else? Where is the date information coming from? By default, the current date and time will be used to store information about the events. So when you send an event, it will automatically store the current time, unless you maybe change that. Got it. Okay. So at a high level, I understand, kind of, what they're doing here, just building a nice UI on that. On our side for the setup here, I've just got a blank Directus project. Nothing in here, no dashboards, any of that. We'll we'll certainly dive into it. But for now, let's dive into Figma and actually sketch this thing out. Right? One of the the big things that come up in my mind is the difference between metrics and something like events. Right? So metrics are like an aggregation. Could be of of, like, potential events or other data. Right? So a metric is typically on, like, well, I I won't say typically. There's all all types of metrics. You know, on, like, a a website or a server, you might have, like, time to first byte, API response time that you're tracking as a metric, and maybe you wanna report on that every few seconds. Right? On things like billing data, you know, you may have, like, let's say, AAR, our MRR, new users, monthly users, etcetera, you know, something like that. On the CRM side, you probably got pipeline, number of new deals, qualified leads coming in, that you're tracking. So those would all be, examples of metrics. And then the events are the actual things that happen, not on the schedule, I guess. On the schedule. Things that happened, basically. Let's call it that. Right? This could be when a new user signs up for a service that have happened. Things that have happened. Great. Alright. So in in a system like this, it might be helpful to have both of those. Right? Because one of our metrics could be aggregating all the events that happened for that particular day. Page views, you know, video views for Directus TV. Lots of lots of opportunity there. And then, what else are we gonna have on this? Right? Data sources, sources of data. Data sources, this would be, you know, our CRM, our, websites, analytics, billing, accounting, etcetera. Whatever those sources are, it looks like Databox has a lot of those built in. You know, we'll probably be just basically creating our own at this point. But this feels pretty good as far as functionality is concerned. I think there's a there's a pattern that I've used in a previous project. It's It's actually our directus dot pizzademo. A really cool demo if you wanna check it out. Just a a look at, like, a a full fledged direct to this project in in many different facets. There's 5 or 6 different use cases slammed into this thing. But we've got, this pattern of, like, metrics where I've got, my name and description, and I've got, I like some prefixes and suffixes that I could show inside the UI or on a front end, and then I have my data. So instead of shoving it all in one table, kinda separate that out. I kinda like that pattern, so let's do that. Do metric underscore data. Looks good. Alright. I'm just gonna pull this up to the side. Again, this is the use whatever you have at your disposal part. Right? So I've got my Clean Directus project over here. We're gonna create a couple of new tables. Let's do metrics first. So that's the one we're working on here. Do we need a status for this? Probably not. Do I wanna know when it was created, who it was created by? Yeah. Maybe. Maybe we wanna add a sort for it. No big deal. Alright. Then we're gonna give this a name. Great. We can give it a, like, a key if we want, as well. So if I want to have a unique identifier beyond like a UID, You can also, like, set these up to be manually generated strings for the primary key. Obviously, you've gotta enter that every time, but I can go in here and make this string unique. And if we're looking for a key, you know, we might want to make that URL safe and just use the input option to slugify it. Alright. And then we've got, like, a description of the metric. Maybe I'll go in and add a note for this. What does this metric measure in detail? Great. And I've got prefix and suffix on here. Not sure we necessarily need those. We'll just keep that off for now. But basically, we're gonna make sure we unhide this ID. Need this ID when sending data, sending metric data. That'll be the collection that we create next. Right? So we've got a name, we've got a key, we've got a description. Right? We've got API response time over here just as a a reference, but, what am I looking at initially? Let's call this, like, ARR. Alright. Tracks changes to annual recurring revenue over time. Great. Alright, so we got a metric in there, looking great. Now we need a place to store that metric data. Right? So applying that same pattern, I'm just gonna fade that away. Just close this out. Alright. We're gonna have a metric data table. Metrics, data, metric data could go either way. Naming stuff, always the hardest part of these episodes. Right? So on this one, we've got, like, the optional fields here, just shortcuts for things like recording a timestamp whenever a thing is created. Now one of the things that I like to do on something like this is just remove any ambiguity and call it a time stamp, or create it at, but you can change those on the fly as well. So I'm gonna unhide that. For our metric data, we're also gonna have a value for that data. Right? This is gonna be let's go with a decimal on this, just because we might wanna store something with the decimal place. It's not always integers. Great. We'll choose type decimal. And if I wanted to flesh this out even further, I could go to the advanced settings and, like, control my precision and scale if I needed to. If I go into the display here, I can also auto format this, which Directus will try to guess as to what format this should be in and apply some styling and, conditional formatting for you, make that look nice. Alright. So we got a timestamp. We've got metric data. Let's link these things together with a many to one relationship. So Directus makes it super simple. To create these relationships, we're just gonna call the key for this field inside metric data. We're gonna call that metric, and then we have our metrics over here. So that's the related collection. Now I could hit save here, but I'm gonna open up in the advanced settings and I'm gonna go to the corresponding field under the relationship tab. And here I can go ahead and create that inverse relationship in the metrics table. So I'm just gonna call this data just to keep it simple and Directus will show me, hey, we're gonna create this field for you. So if I delete a metric, you know, maybe we do wanna delete the metric data just because it doesn't make so much sense without the identifier and, you know, the name and the description of that metric. Alright. So this looks pretty good. We'll just, make that half with clean this up a little bit. Right? What else do we have? We have events incoming as well. Right? So, again, the difference between events and metrics, like, a a metric is something that is going to be at a specific point in time. An event is something that that happened. Right? So a a ARR doesn't happen. Right? A user subscribes and that has an effect on ARR. So also starting to sound like a pilot or a pirate in this episode, not a pilot. So we got an event. Let's see. We'll, again, do the same thing with the timestamp. The events are probably not going to to change much, so maybe we'll just call, like, created by, like, the user here. That's cool. Alright. And for the event, let's see. What are we gonna have on the event? We'll probably have something like we have on, we'll have a name for the event. Maybe that's just a string. We could have a key for that event. Key. What kind of event key is this? We will go back in, make sure this is slugified, make it URL safe. We got the user. We probably wanna show that in this case. Even though those are hidden by default, we wanna show the timestamp of this event. And then, you know, you might have, like, a a JSON field to pass metadata. So in this case, I'm just gonna pick the code interface, choose the JSON type, and we'll just call it metadata in case I wanna store additional stuff on the events. Alright. That's looking nice. And then, you know, to bring this all home, let's add some icons. But, one of the other things that I'm gonna do is add the sources just in case we could we wanted multiples. Like, I'm imagining if you got your CRM data, you're probably looking at 2 or 3 different metrics within that. Or, like, if you've got your Stripe data, you're probably checking your ARR, your MRR, number of new sign ups, your churn rate, things like that. So, there's maybe one more level of abstraction here that we're gonna go for. Alright. So we've got events. Let's just get chip extraction. That looks good. Metric data, we could probably hide that if we want to, or we just call it data, see what we got. There we go. Data usage. Good enough for me. Alright. Great. So let's add that sources, or this could be something like services. Maybe that makes more sense in this particular case. We'll use the UID. You know, we'll see when users update these services. We'll give it a name, and then we can link, a service to, you know, both of these, like events and metrics. So we'll link, use the mini took one here. We'll call this the service that is related to this metric. And if I flip over to that advanced section, again, I could go in and add the different metrics that are associated with that service. Great. And if we go to events, same thing. Right? I might call this service Services. K. Go to our relationship. We'll create that extra corresponding field. It show the related values in the display template. And now we've kind of got this whole thing fleshed out. Right? We've got ARR, the service here in this case. And maybe this is Stripe as the service. Great. Okay. Looking great. Love it. Love it. You may we do some drag and drop to kinda change the stuff around. See the ID. Don't really need it. But let's go in and actually, you know, populate some of this data now that we've we've got something in here. Right? So I'm gonna go in and create something here, but you could see I can't change my time stamp, so I could manually change that. Let's see. The metric we're using here is ARR, so this is gonna record the current value because of the way that time stamp field is set up, or the current date and time. I'm sorry. We'll do something like $1,998. That's that's what we're making ARR right now in this particular app. Not doing so well, but, if I'm manually throwing these in here, I can't edit this. So two ways I could populate this data. I could use the API to quickly, like, shove data in here for this specific metric if I wanted to. That's available to me. Right? Or I could, you know, remove the restriction here on this to basically let me edit that value if needed. And then you could see on create, there's kind of this save current date and time thing going on as well. So I might do both here just in case we need to edit that. I'll change that. Save it. Cool. Alright. And then, you know, if we change this again, we go to change it to 8:8:8, maybe last Thursday. That's what our ARR was. And you can see that time stamp was populated, so I could turn off that behavior. But if I just hit the save button twice here, we'll get some actual data from this. But if I wanted to do that via the API, that's all ready to go for me as well. The only thing I need to do is either use a static token or make that publicly available. Right? In this case, I might just create a static token. So I'm gonna go into my user directory. We'll say, scroll down to the bottom here. We're gonna create a static token to use as we're calling this. And I'm just gonna pull up a little app that I've got called Bruno. It's kinda like Postman, except it is offline first. So Postman, you have to be logged into their service. Bruno is, I think it's open source and offline first, basically. Like, it sorts all of your stuff locally. So, what is the name of this? This is metric data. Right? And we've got our metric that we're using here. So I could copy this. And And as far as the payload, we should just need to pass, like, a timestamp and a value. So this is using the, timestamp value. So we've actually got the time zone in here as well. But if we go in, we're just gonna do 8055 post metric data. Let's set this up as a post request. My headers, I've got, like, a Directus token set up already for this. I'm just gonna replace that value. Is it secret? Secret for now. There we go. We'll save. Changes saved successfully. And let's actually just see if we can get the Directus data first. Says my token is incorrect. Correctamundo. That's not great. Let's try this again. Did I actually save the user? Maybe I saved the wrong user. I think there's only one user, but alright. Delete. Save. There's the value. I'll hit save. Try this one again. There we go. So now we can see our actual metrics. Right? That's the beauty of Directus. Take any SQL database, wrap it, gonna sit alongside of it and give you APIs in order to access it. But now I could go in and I should be able to post this with just like a raw JSON body. Got value of, what, 777. The metric, we could just copy that. Right? That's the unique identifier for that metric. I could have set that up to be, again, a manually entered string. That might have been easier for this. And then we're gonna add a time stamp to this. So if I populate the time stamp, it should do that for me. Let's just dig back in time here and do that. So we'll send that one. Now if I refresh, it should have a couple of metrics, 777 generated less than a minute ago. Right? So it's not saving that timestamp. Maybe we just need to actually turn that off for now. So we go into our metric data. Just turn this off. Where are you? Save current on create, do nothing. Alright. So now if we try this again, you can see that the date that's being stored is that 22nd that we had. Just mix this up a little bit. 555. Dig back into time. Great. Alright. Sweetness. Okay. So now we got some data. Right? Let's build a dashboard out of this data. So we're gonna go into the insights module inside Directus. The beauty of insights is that I can build dashboards with low code, no code. Really, it's no code unless you start needing to adjust some of the the JSON within the filters, and use some dynamic variables. But beyond that, we could create these dashboards really easily. Right? So we'll call this the our dashboard, and we've got a do we have an no. Let's do it like a chart. Chart. See what we got. Yep. There we go. Got a chart going on. Let's start adding to this. Right? So if I look if we do I have the data box up? Let's just see what they've got. Like, first on the list here is, like, revenue. I see new customers, like, website sessions, all pretty standard stuff. We're we're just gonna go in and let's just show, like, a a metric. Right? Like, what is our current ARR? If I wanna send this out to, you know, one of my team leaders or, a CEO or, you know, my manager, like, how how are we gonna set this up? So in this case, the collection that we're working with is the actual metric data. That's where we're storing the values that we wanna see. The field that we wanna report on is the value itself. And the aggregate function here is basically, like, what are we gonna run to get this specific data? In this case, you know, I really just want, like, the last value. As of, like, the last value in that series, what's our ARR currently? So I could do this, and the sort field here would probably be timestamp. And then the filter, basically, like, of all the metric data items in there, how do we select just the ones for ARR? So I'm gonna go in. We've got our metric thing here. You know, I could go by the key here, that the key is equal to arr. Hopefully, I set that up, we'll find out in a moment. And then I'm just gonna scroll down. We'll give this a name, like current ARR. Great. And we could see that. So we got 777. That's the last value in that series. Right? Maybe we add a prefix, so we get dollar sign. But let's just check that. And if we look here, 4 minutes ago, we have 777. Now if I were to go in and delete that value, right, that should become 999 when we go to the dashboard. Okay. All is right with the world. That's that's looking nice. So this is a little underwhelming though. Right? We wanna see more data than this. So actually, let's let's add some time series data to this. Let's track those ARR changes over time. So the time series is good for this. You could also use the line chart, that will allow you to get multiple groups. So if you wanted to, kinda look at groups of users within a a certain time period over, like, a certain set of events, that sort of thing. So, again, we're going to choose Metric Data. The aggregation here, if I've got 2 values for a given day. Right? This is probably the way that I'm gonna report on this, is by the by a given day. Do I average those values together, or do I do the minimum or the maximum? Let's just average them together. You know, one of the things that we might set up is just a a flow or an automation to go in and run those metrics for us on a daily basis. For the group precision, we'll just use day for this. Date field, we got our time stamp. And when it comes to the date range, this is, a little tricky in that, like, if I select one of these, it's always gonna be that specific date range unless I do something like automatic based on data. Or, you know, I could fill this in with other and make it like a dynamic variable where I could pick past 30 days, past 14 days, etcetera. In this case, what I'm gonna do, I'm gonna just set that to be automatic based on the data, and the value field here is gonna be the value. Plain and simple. We'll do ARR over time. Great. Looking nice. Okay. So now we can actually see those changes. And again, this is a full drag and drop builder. One of the nice things here is you could see if I connect these 2 together, the little border radii, border radius, those all they connect nicely. It's very subtle detail, but very helpful. Alright. So now we got a chart. You know, we could make this type of thing dynamic as well. It looks like the Databox dashboards are relatively fixed. I see like month to date for a lot of these. But let's say if we wanted to zoom in on a particular timeline. Right? You know, hey. This is only data for May, but, you know, maybe I wanted to look at last month's data. So what we can do, you can make these things dynamic using variables, like either the global relational variable, which allows you to select a specific thing. In this case, maybe you wanna report on, a specific metric. Or like a global variable here could be based on time. Right? So we could do something like this where we have variable key. We'll do date from, and that'll be a time stamp. Great. Use the date time interface. In the US, We don't use that 24 hour clock very often. Alright. So let's call this date from. Alright. So now we get like a panel here. We could duplicate this thing. We can call it date 2. So in the variable key, we'll call it date 2. I'll make sure I change my panel header just so I don't confuse any users of this dashboard. But we're all about empowering people. You know, if you've got, like, a ton of internal data, you could potentially and it's in a SQL database. You could connect Directus to it, get APIs, and get a dashboard experience out of the box, right, that that all members of your team would be able to use. So now that we have created those relationships or those variables in this case, these are not relational variables, they're just global variables. I can use the names of those inside my other data. Right? So current ARR, we're always looking for that to kind of show the same data. Right? What is the current ARR? If we're talking current, it's always x. But in in the case of like over time or like showing a list of records, maybe I want to filter that for a specific date range. So I could do something like this where I say the timestamp, I've got these filters here that you can see. But we also have greater than or equal to. And I could do something like this, is between, and I use a mustache syntax. And now we can use those date variables. Okay? So now I'm not seeing anything, right? What do I change this to? For default value, we have some dynamic variables here too. So let's say, for the date from, maybe we change it to the start of this year. So just back up to Jan 1. That's great. And then for date 2, maybe we always wanted to show, like, the latest value. So we could just go to something like dollar sign now, which should populate the current time stamp in that particular field. If I just refresh, now you could see what we've got going on here. So if I back this up, like let's go to like May 23rd. Okay, so now you could see like the scope of my data changes within the chart. That's how I can make these dashboards dynamic. Really, really neat feature. Right? Alright. So going back to our our board here. We've got some metrics. We've got metric data. We've got the sources planned out. We've got events. We haven't populated any of those. But one of the things that I kinda jumped the gun on here was what do we actually want out of this specific application. Right? We wanna manage all of our metrics in one place. That's right. Directus becomes the single source of truth for all of these particular metrics that we are managing, that we have under our control. And what else? So we're gonna build a usable dashboard for these metrics with usable dashboards. Yeah. We could slice this a couple different ways. And then as, a larger goal, you know, maybe we wanna integrate some third party service. 3rd party service. Alright. So let's take a look at, like, the events side of it. Right? Events would be very helpful, I created a new account. Right? So let's just call this 1, created a new account. Alright? So let's just call this one, like, new account registration. Yeah. Seems great. New account and registration. I've got metadata here that we could store. It's great. We'll just pick the service. You know, maybe that's we create a new one. Can we create a new one from here? We can. Let's just call it website. Great. Okay. And cool. So now we got an event. The name is no account registration. It happened at this particular time with this particular user. I could go in and maybe I just create, like, a a fake user as well to populate some of these events. Fake user Fake user 2. That's a real original name. Right, Brian? Yeah. Okay. Alright. So we got another event. Maybe we want to, in this case, I'm gonna just disables the read only part of this. And oh, I didn't save it, did I? Disable Disable, just in case I need to change those items. Alright. And the difference here is we're gonna assign this to fake user. Let me go up to the 3 dots. If you are pretty new to Directus, this is a helpful hint here where you can use these keyboard shortcuts, command s to save and stay. Or let's say you wanna create a new record. You could save and create new. In this case, I'm gonna save as a copy. So we're not gonna touch the original event. We're just gonna create a copy of it, and I'm gonna assign this to fake user, account user registration, blah blah blah. If I was doing this on my front end, I would certainly be passing, like, some metadata of, yeah, what account let me maybe some of those account details that would be relevant. Of course, not any personally identifiable information. Don't wanna store any of that. And then we change the timestamp, and we change that to May, etcetera. Okay. Alright. So I've got a couple events. Right? And now, let's say I needed to run a report that shows me all of the new registrations within a certain time period. Right? Now I could go in and have, like, basically set up a a chart based on that. Right? And I could use this event data to get a list of those events or kinda aggregate those. But how would you track the the changes between those things? So, you know, how do I show on a dashboard how many new registrations there were 2 weeks ago versus today? Right? Or how can we analyze those things under time and compare them to other details? So in that case, maybe we want to convert those events over into metrics. One way you could do that is with direct as flows. Some automation inside the system. Right? So every day, maybe I want to go in and summarize all this information and store it so we can make quick work of analyzing the data. So in this case, let's just set up an automation. Right? We're going to summarize registrations. Sounds good. Alright. For an icon, You know, my recovering designer in me always has to have an icon. And for the trigger here, we're gonna trigger this at regular points in time. I always get hung up on the Kron syntax. Kron syntax for every minute. Let's see what we come up with for this. Every minute. Okay. You're probably gonna only set this up to run once a day. I'm gonna run this once a minute just so we can, like, get a good look at it. And you could even, like, manually trigger this sort of thing if you wanted to. But in this case, we'll set that up. And now what do we wanna do with this? Right? So if we're gonna summarize this registration data, we'll go in, and we need to get that registration data. So we'll say get data, and I could go in and hit read data. So we're gonna get the items from the database. Let's use full access, which gives it full permissions instead of the ones from the user, from the trigger, or the public role, in this case, like a cron job. We'll go to our metric data, and we can either specify the specific IDs we wanna find, or we can set up a query. So these are just the standard query, like, global query parameters that we support via the Directus API. Just flesh this out. Where are, which metric are we looking for? We want the metric. So here, we're at our metric data. I can also dig deeper into the actual related fields. Right? So the related field, metric key equals let's say equal no. Metric key. Yeah. I think it would be something like this. We'll see if this actually works. Key equals arr. No. Okay. Alright. So we look for the filter. This is a global parameter. So within the metric data, we look at the metric, which normally is just stored a UUID for that. But we can also go into the related fields here and say the key of this metric is equal to ARR. Is that actually gonna run? We'll see. Right? So we can see this already ran twice. If I wanted to to sit around for another minute, we can take a look at it, see what happens, or, you know, I could manually trigger this thing as well. So let's do that. We'll just go in metric data. Cool. Summarize registrations. Alright. I'll hit save. And now if I go into metric data, I can also run this thing. I forgot to turn off the need to select something. But now if I run that, we could see if we actually got the data that we were looking for. Great. I can see this is the data that we need. And just to make sure that's working as intended, I'm gonna just add another one here and maybe maybe just leave off a metric to make sure we should have 4 values inside that flow instead of 5. So, again, I'll just run this flow. Great. I'll go back and look at the logs really quickly. And I can see I'm only getting 4 values here. That's great. Cool. Alright. So now maybe I want to summarize those. But this is not our I'm actually looking at the wrong thing, aren't I? I'm getting the data from our metric data and not our events. So we need to make sure we actually change that up. Just an oversight here on my part. So in this case, we're gonna do the key equals new user registration. Alright. Now let's test that again just to make sure this is actually gonna run. K. Having a look. Alright. No. Are we getting our data? Did we call it something else in our events? New account registration. Always remember what you name things. It's a challenge. All right. Just paste that key here inside our query and hit save. Alright. So, again, maybe we wanna change where we're actually running this from to. Go to Events. Alright. Lots of back and forth here. So I'll select all these just because of the way that this flow was set up. Normally, I would set this up to be a cron job like we had before. But now if I check the logs, we could see we've got all of our events here. That's great. Now we are going to collect all those events and save them as a metric, right? We could also specify, You might want to use our dynamic filters as well, right? So let's actually just save this first. We'll come back to it. So we're gonna format this. Let's just do a quick run script. We'll call this like format data. And if we see here, we've got, this is our actual data. All right, so I'm gonna just copy paste this into Versus Code here, so I've got it. And then inside this operation, this run script operation, we can run arbitrary JavaScript to do whatever we want. So in this case, we are just going to actually combine these two pieces of data. Like, summarize how many we just want to get a count of all of the events for this, right? So something like this where we have, we're gonna get the data. We call it get data. So we could say that const events equals data dot get data. As each operation within a flow runs, it appends the data that operation returns under this key. So after this step, the results I'd be able to access via data dot format data inside a run script. But in this case, we're just gonna do const events. And then maybe we just return the the length of those, I think. Right? Simple as that. We don't have any values that we're like summing up here. We're just going to return events dot length. And this could even be simpler. It's basically just return dot length. Right. And actually maybe we do something like this where we say this is the metric data. The value is data dot get data dot length because we're returning an array there. The timestamp would be, what, new date. So we'll get the current timestamp. And then the service Okay. Maybe we'll open this up in a new tab. Just duplicate this. We'll go in and like our website service, I could pick this off up here. Or if I look in the information, no it's not up there. I'm just gonna use the URL to grab this specific service. Where do we go? Alright. So we'll populate that service ID. And then we have the, what, metric. So we're gonna have a metric attached to this, which we have not populated yet. So let's just do that as well in this second tab. We'll go in and create a metric. We'll call it new account registration. New account registration. Okay. Save and stay. And then I get my ID for this metric. Just paste that in there. Great. So we can kind of see the the format that we want here. And, actually, we don't need the service because that should be on the, the actual metric here. So this is gonna be the metric data. And we're just gonna return that value, return metric data. All right, great. And then the last step of this puzzle would just be to go in here and create that data. So we have formatted the data that we want. We're just going to create metric data. We'll use the metric data option here. We've got our full access. And for the payload here, I'm just gonna do this, where we say format_data in like a mustache syntax. Doesn't seem to like that. Maybe we wrap it here. Okay, all right, so now let's actually try and run this flow to see what we get out of it. All right, so I'm here on my events, Right, we've got 2 events. We should get like a summary for those. So if I go here, now we should have like our Metric Data. Okay. 50. No, that's not correct. That's the one we manually created. Let's take a look at this and see where things are falling out. The time stamp. Oh, we're not getting the proper value for the timestamp. Alright. Let's take it. Do we need to save that to ISO string? I think that's maybe what we need. 2 ISO string. Is it 2 ISO string? Let's just take a look. 2 ISO string. To, okay, ISO is capitalized. I always forget about that. All right. There we go. To ISO string, new date to ISO string. And we might even do it like this, just to new date. Date. Not data. To ISO stream. See if that gets us where we want to be. So we'll just go back to our events, run this flow, summarize our registrations, see what we get out of it. It's great. Create our metric data. There it is. That's all running, right? So now I could switch this over back to a CronJob. Let's call it Cron. Yep. Set this up on an interval. Save it. And, you know, now in the background, this is going to run. It's gonna summarize all those events for our registrations. Right? So now I got I have the individual events that we can see and, you know, I can potentially act on those individual things when they come in. You know, maybe I wanna send those to Slack for our sales team to follow-up on. But also on our metrics. Right? Within our dashboard, I can see I've got our data for that specific metric as well. And in this case, maybe I wanna clean this up a little bit if I'm looking at it inside the data studio. So I could go into this. We look for our metric inside the metric data. Just use a display for this, call it name. Great. And now if I look, I could see, okay, this is the new account registration. I could see the value for that. And again, like, we could make this dashboard totally customizable. Right? What if we wanted to change this metric that we're actually showing here? What if I wanted to make this chart variable? So we could call this something like Metric over time. And now inside the collection, before we do that, we're going to create a global relational variable. I'm gonna call this key, the metric underscore key, just so I don't get those confused. But we're gonna use the collection metrics here. We're gonna pick a single metric that we wanna use. And we're just gonna say pick a metric to Report on. Great, okay. So we'll add this into our lineup. And here, I want to show just items from the metric that I have selected, right? So how do we make that work? I just basically go in here. I'm going to toggle raw data. I'm going to use that same mustache syntax. We're going to say metric_ key. Alright. Let's take a look and see if this is actually gonna work out how we want it to. Alright. I don't see anything going correctly there. But if I look for new accounts, no. That's not doing what we want. Right? So let's go in and actually fix this. This is not the way we wanna do it. Right? We still wanna report on that metric data, but we want to only filter on the metrics that are selected there. So we're gonna do this instead. We're gonna change that to our filter. And the metric ID has to be equal to the metric key that we use from that variable. So you can use just regular variables like date or string or something like that if you wanted to. Or you can use relational variables. So the other thing that we might set up here is like an AND filter. This should be AND by default. But let's see what we get, right? ARR. This is still not populating correctly. We've broken something. Our date and value fields were not set. So let's test this out one more time. We get no data. If I pick a metric, now we can see our ARR And maybe I wanna change the display template on this as well, where we're just showing the actual name of that. There we go. All right. So now I can see, hey, we're looking at our ARR over time. Or if I wanna check out our new user registrations, we could see those, just 2 on this particular day. Great. And let's just change this a bit, right? We'll do like 6 previously. Let's put that like last Tuesday. Save as a copy. And now if we go back to our dashboard, start to be able to see some of this, right? So we can see the variance there. Great, it's a good way to do it. Alright. So same kind of thing. I could potentially call a third party service using Flows if I wanted to. So, you know, there are multiple ways to do this, but I could go out. And if we use Stripe as a good example, I guess. Running out of time, so Stripe is gonna have to work. We'll just go to Stripe. I'll log in. I'm going to look at just some of my test mode data. Which account am I in? Let's get into one of my test accounts. Boom boom boom boom. Alright. Developer mode. There we go. Okay. What are we gonna need from this? Right? We're probably gonna need a key. API key, create a restricted key, key name, test key. What do we wanna look at? Invoices. Read invoices. Permissions. Okay. Smart enough. All right, reveal test key. I'm going to copy this down. And let's look at Stripe API list invoices. List all invoices. Alright. So let's do this. We are going to get today's invoices. Maybe we just wanna show what we brought in on that particular day. I'm gonna, again, trigger this manually, not requiring a selection. Great. And then inside Flows, I could just add regular HTTP requests here. So we'll say call Stripe. Now a, like a more in-depth way to do this would be to create, custom extensions for things like this, where, you know, you've got more functionality built into it. But I could just go in and get a list of all the invoices here. Let's see what the curl is gonna be for this API. Alright. This is just gonna be a get request. We're gonna use the authorization header. Use bear. And I'm just gonna paste my token that I've got there. Alright. Cool. As far as the request body, do we really need anything there? No, I'm not sure. Let's just go in and test this out. Right? Get today's invoices. Go back to flows. We'll see what ran did we get some actual data. Status, okay. We get a list of the data coming back. And cool, all right. So what I'm going to do, I'm going to just pull this data into, again, Versus Code. And that just helps me traverse it easier when I'm putting this together. Alright. Alright. So today's invoices, you know, let's go in and make a new metric for this. Today's payments, today's invoices, invoices today, daily. That's great. Whatever we wanna call it. We'll add our Stripe service to this, just so we know that's linked to Stripe. We'll hit save. Copy this metric ID. Cool. And a couple of things, right? What are we going to do? We're going to run a script, calculate totals. Okay. How we doing on time? Alright. Let's cheat. GPT, you can type faster than I can. Write a JS function to summarize, to calculate the invoice total for this data, the total of all invoices. Total of all invoices. There we go. So this is gonna spit out a function. It's gonna be a reduce function, but hopefully, it figures out the exact structure of the data. Give it a shot. Right? Alright. So we'll just paste this here. This looks to be correct. Invoice data dot data. Let's just traverse it ourselves. Right? InvoiceData. Data. Amount due. Okay. Calculate total invoices. Let's do amount paid. Paided. Amount underscore paid. Okay. And then we're gonna call this function. Alright. Return, Let's say totals equals calculate total invoices. And the data here is gonna be data dot, what do we call that? Call Stripe. Data dot call Stripe. How are you on time? We're running dangerously low on time. Call Stripe dot data? Call Stripe dot data dot data dot zero? Is that what it's looking for? The InvoiceData, InvoiceData.data. Oh, it's looking for that. Okay. All right. That's what we'll do. We'll return payload to update this. So, oh, forgot to close that off. Okay. We're going to return, what, a symmetric. Okay. What's gonna be our metric? There's our there's our North Star metric. There's the ID for that. I think I'd already copied that before. Okay. Then we've got the value equals the totals and the timestamp equals new date to ISO string. Alright. Can we actually get this first shot? I don't know if we will or not, but we'll try it out. Create Metric. Okay. Calculate totals. That's what we're going to stuff here in our payload. And we will run this thing on metric data. Full access. How we doing? Time, 42 seconds left. We are chugging along. Alright. Get today's invoices. Did this actually create our metric for us? Yes. 30,000. There it is. Bada bing bada boom. If we go into our dashboard really quickly, we should even be able to report on this, today's invoices. We could see that value of $30,000 even though it's not inside the well, we don't have any other data points, so there's not, like, a a necessarily a chart here to show. But within 10 seconds left, bada bing bada boom, we're calling that a win. We have managed all of our metrics. We have build usable dashboards or built usable dashboards, and we have integrated Stripe as a third party service to pull in our KPI dashboard. Right? This is a little underwhelming here, as far as, like, the actual dashboard. That would be pretty easy to flesh out, though, as soon as I started populating all of my data. So for me, that would be my next steps. I've hoped you'd enjoyed this episode. Hope to see you on the next one.","454d776b-08f8-426e-a630-93af2026cbc5",[500],"9c1ee329-fcd4-4b1c-9d29-55c8ae9b932e",[],{"id":157,"number":158,"show":122,"year":159,"episodes":503},[161,162,163,164,165,166,167,168,169,170],{"id":168,"slug":505,"vimeo_id":506,"description":507,"tile":508,"length":219,"resources":8,"people":8,"episode_number":323,"published":509,"title":510,"video_transcript_html":511,"video_transcript_text":512,"content":8,"seo":513,"status":130,"episode_people":514,"recommendations":516,"season":517},"intercom-messenger","954528032","It's a race to a real-time chat widget in this episode. Bryant tries to build his own version of the Intercom Messenger - a business messaging widget that includes live chat, helpful articles, and other rich content.","28671146-5bfb-411f-9bea-a57589a931a0","2024-06-07","MIssion: Intercom Messenger Clone","\u003Cp>Speaker 0: Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate here at Directus. And if you're new to the show, the basic gist is this, we take 60 minutes and we try to build or rebuild some of your favorite apps or some of your wildest app ideas, in that 60 minutes, we achieve our stated goals or we publicly fail trying. And by we, I mean me.\u003C/p>\u003Cp>That's okay though. When I fail, I fail spectacularly, usually. But I've got 3 little girls so I'm used to being hobbled. That's okay. There are just two rules.\u003C/p>\u003Cp>Number 1, you have 60 minutes to plan and build, which does pose some challenges. There's no more, no less. And number 2 is you use whatever you have at your disposal, just like in the real world. You know, make it work. Figure it out.\u003C/p>\u003Cp>And in today's episode, we are going to take advantage of rule number 2. So what are we building today? We are working on the intercom Messenger. Right? Intercom is a, like, help desk, onboarding tool, messaging tool.\u003C/p>\u003Cp>They've got all these things that are wrapped up into it. It looks like their latest positioning is, like, customer service, which is great. But like most of their it comes from this little messenger widget over here. Right? So it's not just live chat.\u003C/p>\u003Cp>It is kind of like a rich format where they can check product news, they can chat with the support team, there's bots, there's AI, there are articles integrated, you know, there's, like, product tours and stuff like that. The the main things that we're concerned with here are this kind of nice interface for this, and then number 2, like this actual, like, live chat functionality and maybe the ability to add articles. So we keep the user in context, That's what we're gonna be tackling today. I I think it's doable, not a 100% sure. Let's go in and start the clock on this.\u003C/p>\u003Cp>We will start our timer, 60 minutes and away we go. Alright. So as far as what type of functionality we want out of this, you know, what are the the jobs to be done or what are the goals for this specific project. We want to be able to chat chat with visitors in real time and then, you know, maybe display rich content inside the messenger. This could take the form of articles, help center stuff, updates, etcetera, and I totally cannot spell.\u003C/p>\u003Cp>How are we gonna get there though? Right? Normally, if you've watched any of the past episodes, at this point, I'm like doing some data modeling. The starting point for today is a little different in that I've already got a project that we're going to lean on. It's called Agency OS and it already has a a fully fleshed out data model.\u003C/p>\u003Cp>You can explore the front end of this at agencyos.dev. But this actual project you can download and test out yourself. It lives in our Directus Labs organization at, Directus dash labs slash agency dash os on GitHub. Basically, there's some instructions, but the main features of this include a website, CRM project tracker type of thing, and then you have a client portal. So on the front end, it kinda looks like this, your standard agency website.\u003C/p>\u003Cp>You know, we've got all these different blocks. It uses the page builder inside Directus. We got blog posts and articles. You know, there's a a help center that should be displaying some articles that doesn't. And then you have, this client portal where clients can log in, keep track of their different projects, see files, see their invoices, pay for those specific invoices.\u003C/p>\u003Cp>Right? This is this is pretty neat functionality. And then, you know, you would basically manage your entire workflow from your deals all the way through, like proposals, projects, and tasks inside your direct assistance. Right? So I can see a list of all the tasks that are necessary to complete this specific project, who's the organization, who are our contacts for that organization, And I can manage this whole thing here.\u003C/p>\u003Cp>But in this case, I'm using this because we've already got this conversations model and this messages collection. And we have things like help center collections and help center articles already mapped out, which is is really nice, just to kind of piggyback off of what I've already built in the past. So if we dive into conversations, you know, those are gonna be I can have multiple conversations ongoing at once. Those would be like the different threads. Inside that we've got like a user that created it.\u003C/p>\u003Cp>Maybe a visitor ID in this case, if the visitor is not known. We've got some messages inside there. Inside the messages, each message has a bit of text, who is the user or the visitor ID that created it, and away we go. That's what it looks like. We're gonna dive into like some of the real time features of Directus as well.\u003C/p>\u003Cp>So what have I actually got set up for you here? In starting this project, I've got a Nuxt application that I often use as a starter here for this. You know, the Nuxt application just has a direct us client using the SDK already preconfigured with, authentication and rest. I've got some login and register pages, so we can kinda simulate that. But that's kind of it.\u003C/p>\u003Cp>The other like 2 minutes worth of setup that I have done, this Agency OS project, before the V2 release, I had a janky little chat widget that I was trying to use in this specific project. So I can go back in time here and find this little chat widget. Let's just copy this in. And, it looks like I've already got it copied in, and cleaned up. But just a little chat box that we could use.\u003C/p>\u003Cp>And, I could never quite get this to work before the big release, so, it it ended up getting scrapped. So let's revisit this. I've got our chat widget. Let's drop this thing into our index page. So if we open up this incognito window so I don't spoil any of the, so I'm not using that direct to session token basically.\u003C/p>\u003Cp>Alright, so I've got my page here, I'm just going to drop this chat widget in there. And now I've just got this shell for a chat widget. It opens, it closes, it kinda looks nice. That's really all it does, right? There's this motionable component that uses the view motion View Use Motion, composables.\u003C/p>\u003Cp>Where is it? View Use Motion at view use slash motion. Just to get like some smooth animations using, I think it uses pop motion the hood. And it's just, tailwind for styling, right? Our content is gonna go in here.\u003C/p>\u003Cp>But let's fulfill that first thing, right? We wanna be able to chat with visitors in real time. I don't even know why these are bold, that's gonna aggravate me. Alright, how do we chat with visitors in real time? Directus has powerful real time functionality built in.\u003C/p>\u003Cp>Now inside my Docker Compose file for this, I've already got real time enabled. So that is the WebSockets enabled part here. And when you're working with real time and direct us, there's 3 authentication modes. There's public, handshake, and strict. Public just means anybody can open a connection.\u003C/p>\u003Cp>Handshake means that you don't have to provide the authentication as the you could still connect but the first message has to be authentication. And then strict means you have to pass the authentication before you actually initiate a connection. Alright, so what we're wanting to do here basically, we're gonna have to extend our plugin that we have. So I'm gonna go into my Directus plugin that I've got. And again, it's just creating a simple SDK client to work with.\u003C/p>\u003Cp>I'm gonna go in here and let's actually create a second client. Create Directus Real Time Client. So I'm just gonna create a new Directus client and I'll just copy paste this previous client. Great. Except with, we're not gonna use any of this, we're gonna use the real time composable.\u003C/p>\u003Cp>Alright? And there is an auth mode property in this case and I'm just gonna pass public because we want anybody to be able to connect to this. So I'm gonna create that client and then the next thing that I'm gonna do is provide that client to you, the Nuxt application. Let's just call it directus sws for directus web socket. And we're gonna pass directus ws.\u003C/p>\u003Cp>Okay. So now we have created this client. Our chat widget still doesn't do anything. That's fine. What we're gonna do is open up chat widget and we're going to access that client.\u003C/p>\u003Cp>So we'll do directus ws, because Nuxt provide when you use Nuxt provide it adds a dollar sign to the front of it. And we're just going to do use Nuxt app composable. That will give us access to our web socket client. And then we're going to actually open a connection, right. And this application is using like server side rendering inside Nuxt.\u003C/p>\u003Cp>So I probably want to like wrap this up inside a composable as well like the like an on mounted hook. So we'll await the Directus WebSocket connect. And I I don't work with Directus real time a lot, so we may get some funky behavior here that needs to be async. Okay. So I'm just gonna open up the console and see if I open up all and I go to web socket, we can show a connection here.\u003C/p>\u003Cp>It says switching protocols. Okay, AWS local host. Alright, so looks like we've got the WebSocket connection opened. Alright, now let's just kind of dig into the documentation a little bit. Because I the next thing is gonna be, like, subscribing, but we probably need some message handlers.\u003C/p>\u003Cp>So we'll go into get started with real time on the documentation. I am doing this, just as you might. So you can see that we've got the real time client here. We've opened a connection to the client, or at least I think we have. And then now we've got like this client.\u003C/p>\u003Cp>We need to listen for changes. Right? So I might as well just plop this in our on mounted hook. And I'm just gonna replace client dot with dollar sign directus ws. And now let's see what we actually get out of this.\u003C/p>\u003Cp>Do we see any actual messages here? I don't know why am I not seeing any messages. Directus cannot connect when state is open. Do we need to do we actually need to pass the auth mode to this? Is that an actual property?\u003C/p>\u003Cp>Switching protocols, directus.ws.subscribe. Let's subscribe to messages and see what happens. On message data dot undefined. Message. What if we just console log the message here?\u003C/p>\u003Cp>Console log message. Alright. Alright. So we see we're not getting anything there. Subscribe error, invalid collection.\u003C/p>\u003Cp>Okay. So not getting the correct collection. Let's just take a look at this. This is messages. Oh, let's try subscribing to messages, right?\u003C/p>\u003Cp>On open console log event open. I don't know why it is not not sure why it's not working correctly. You do not have permission to access this. Right? So as a public user, we don't have access to the individual messages.\u003C/p>\u003Cp>Alright, so two ways around this. We could potentially provide some authentication, if I can actually talk, right, or we could give access to this. Alright. So if I go into my access control settings, I'm gonna go to public in this case, and I'm not gonna give all access, but maybe we use custom. Right?\u003C/p>\u003Cp>2 things here, we get the public can create messages, that's fine. But when it comes to reading messages, we don't want people to be able to read other people's conversations. So in this case, let's add just a rule to it. If the conversation, conversation visitor ID contains what? Can we even do that, right?\u003C/p>\u003Cp>We'd have to pass the let's do this instead. We'll use our website API user for this. And okay. We'll do messages. You can see conversations.\u003C/p>\u003Cp>I tell you what, let's just open the floodgates here just to test this out and make sure we can see these things. Alright, so we've got a subscription. There's our data, okay. And if I open this up and I add a new message, right, which is the test message, save this, we should see that data come through, which we do over here. Right?\u003C/p>\u003Cp>So we see our subscription for the messages. Do we see our actual data here? Great. There's our text, test message. Here's the user that created that.\u003C/p>\u003Cp>That's gonna be me, the admin user. And we've got the date created. Right? We've got all the details of this specific message. Alright.\u003C/p>\u003Cp>So at least now we are getting the messages. Alright, if we go back to the documentation, we're getting some messages here. That's great. You know, a couple things when we create a subscription, right? We can choose what event to subscribe to.\u003C/p>\u003Cp>So the event wise, we'll probably do like create. And we can even add a query, right. So imagine we've got a website visitor. We want to basically, like, create a unique session or a unique ID for that visitor, so that that user can only see their messages. Right?\u003C/p>\u003Cp>So what do we need to do there? This might be something like a Nuxt Middleware. Do I have do I have UUIDs in this? Okay. So I've just got like a function in here and and this is, like a lot of this is actually ripped from the AgisUS project to begin with.\u003C/p>\u003Cp>But I've just got a function here for generating an ID, which basically just creates like a UUIDv4. Alright, so what I'm gonna do here, let's just create like a session dot global dot ts. I'm gonna add this in the middleware folder inside Nuxt. And when I do this, if I use dot global that will run on every route. So I'm just gonna go here, I'm gonna copy this, and I don't really need to here.\u003C/p>\u003Cp>Basically, anytime we navigate to a route, we wanna do some things. Right. We want to, let's check like a session ID or visitor ID. Visitor ID equals use cookie so we can set a cookie that way we can access it server and client side. We got visitor ID.\u003C/p>\u003Cp>If visitor ID dot value, if there's no visitor ID dot value, we are going to set up a where is it? I'm gonna do Visitor.id.value equals generate ID. There. Okay. Alright so now basically we just created a middleware that's gonna run globally on every route within this application and it will create a cookie for the visitor ID.\u003C/p>\u003Cp>Okay. And now I can see that here that I've got a visitor ID cookie which is some random UUID value. If I delete that, refresh, we can see we get a new visitor ID. But that cookie will persist across sessions, which is nice, and we don't have to do anything else there. Okay, so as far as our query, if we just go back to the documentation, right, we can add a query, and we can do all of the same query params that we would in the regular Directus API.\u003C/p>\u003Cp>So we do something like filter where, well actually let's back up a minute, we need to get that cookie, right? So visitor ID is equal to use cookie visitor ID. Okay. And then for our filter, we want the messages. Let's think about this, right?\u003C/p>\u003Cp>We're gonna have to create a conversation. Let's just get some messages first, right? So we're gonna add the visitor ID to each message. Alright. So maybe we just wanna be able to see we're gonna have to like create a conversation.\u003C/p>\u003Cp>There's no way getting around that. Alright. How can we create items, right? So we can use that same connection and then we can actually get all of our like CRUD operations. So let's just do this.\u003C/p>\u003Cp>We're gonna actually set this up. I wanna subscribe to 2 items, right? Conversations. And in this case, we want to filter where the visitor ID is visitor.id.value. So inside our data model, right, when we create a conversation there's a visitor ID for it so we could track that.\u003C/p>\u003Cp>Great. Okay. So I could see that there. Then we have the individual messages, and we we've honestly got a lot of extra stuff that we're probably not even really gonna need here. But we got visitor.\u003C/p>\u003Cp>Id_id, that's what it is inside our database. And then we've got the cookie value here. So visitor dot ID, and then the param is gonna be like this, equals to visor.id.value. And then I think we can also pass a UID to this subscription. Let's look at it.\u003C/p>\u003Cp>Learn more about authentication, learn more about subscriptions with WebSockets. Alright. Using UID's type is subscribe multiple subscription UID. Okay. So we'll call this UID And this will be visitor conversations.\u003C/p>\u003Cp>Thank you, GitHub Copilot. Let's also open a connection for messages as well. Subscribe messages where messages dot create, query, UID. And in this case we can't just filter out for the visitor messages, right? We also have to be able to show the admin messages as well.\u003C/p>\u003Cp>So in this case what we're gonna do, we're gonna work on the conversations because we can use the related fields inside direct us collection. So I can go up a level here. It's actually conversation. Is what's the parameter gonna be? Right?\u003C/p>\u003Cp>So we could have potentially multiple conversations. Right? So we'll have conversations equals ref, and then we'll have messages. Actually, we might just store all of those. Right?\u003C/p>\u003Cp>Conversation, let's look for our query params, global query parameters for our actual filter here, filter rules, we're looking for items within an array. So we're going to use n, in conversations dot value map conversation dot ID. Okay. I think this should give us what we want. And if I open this now we can see that we've got a subscription for conversations, we've got a subscription for messages.\u003C/p>\u003Cp>I'm gonna just copy the visitor ID here. And the other thing I'm gonna do, I'm gonna create a new conversation. So we'll say new conversation. I'm gonna add that visitor ID here. We'll call this test convo.\u003C/p>\u003Cp>And do we get a message for that? We can see this conversation here. And now let's just test this as well. Will we get, TestConvo not from visitor? Just want to make sure.\u003C/p>\u003Cp>Okay. So you can see I created a new conversation that didn't have that user ID or that visitor ID. And now we don't actually see that. So how are we doing on time? We got about 35 minutes left.\u003C/p>\u003Cp>We're looking okay. So let's start having some conversations. Right? On messages, on init, Subscription init. Get conversations.\u003C/p>\u003Cp>Alright, subscribe on open on message. Okay, so basically all we're doing at this point is listening for messages. And just logging that out, right? So we need to do some other things here. Is there like a is there another property on a knit?\u003C/p>\u003Cp>No, a knit. I need more practice with real time, obviously. Alright. So when we receive the message, let's look and see what our references are here. Full code sample, I think there's actually a guide that I built on multi user chat or somebody built this.\u003C/p>\u003Cp>Alright. Connect cleanup, Subscribe. Okay. So you can get the actual subscription equals await client dot subscribe. Constant subscription equals ws await client.subscribe.\u003C/p>\u003Cp>And then we can get our initial messages. So here, let's just call this get initial combos. And we might also want to grab all the fields within that as well. So we're gonna get all the root level fields for the conversation, and then we're also gonna get all the message values included in that. And then let's populate conversations.value=subscription.data.\u003C/p>\u003Cp>I'm not even sure it's gonna be that. We'll just do it's actually console log subscription. See what we get there. Alright, subscription L2 generator. Cons to subscription.\u003C/p>\u003Cp>Subscription, unsubscribe, generator state suspended. For message of subscription, for await combo of subscription push combo. Subscription is not iterable. Let's just wrap it like in the documentation here and see what we actually get. Right?\u003C/p>\u003Cp>So now if I show let's just show the actual conversations. So we'll go into our content section, and we'll do divv4conversationinconversation. Flex call. Let's just add let's just log that out in conversation. Prepping the pretag.\u003C/p>\u003Cp>Type subscription event, init. Yeah. So this might need to actually be updated. Right? Subscription event, await client subscribe.\u003C/p>\u003Cp>Subscription Subscribe to messages, create event. What am I missing here? Subscribe if type. This message is create, receive message. Subscription started.\u003C/p>\u003Cp>Okay. We're not actually getting the message history there. Display historical messages. Okay. Alright.\u003C/p>\u003Cp>So basically to get all of the historical messages, we need to read the messages. So we'll do client dot, it's actually gonna be directus dot send message, and then we'll get all of the items within the conversations. So that'll just be items. The collection will be conversations. And the action is gonna be read here.\u003C/p>\u003Cp>So we just wanna read all of the conversations. And then as far as the query, we'll set a filter where visitor ID equals visitor ID and we'll just get all of those conversations. And maybe we limit those to like the past 10 conversations, kinda like our documentation here. Alright. Send message.\u003C/p>\u003Cp>Okay. So we're gonna get that message. Send message. There's our items that we could see. There's those conversations there.\u003C/p>\u003Cp>We could see the individual messages within those, but we're not actually populating that data somewhere. Right? So we're going to need on our message function here, basically if the data type equals items, okay, so when we receive a message, we are going to take our message. Alright. If the message dot type conversations there we go.\u003C/p>\u003Cp>GitHub Copilot knows what we want. And we'll just refresh when we open the connection. If I go in and we look for chat widget inside the dev tools, do we see our conversations? We don't. Alright.\u003C/p>\u003Cp>Why not? Message. Message, message. If let's just update this as well. Is connected.\u003C/p>\u003Cp>State is not open. Okay. So we're getting our messages. They're just not populating to what we want. Right?\u003C/p>\u003Cp>So we see the data is an array. That should be what we want though. If we open this up, we should be seeing those conversations. Direct to send messages, items, conversations, if equals conversations, conversations dot value equals messages dot data. Conversations.\u003C/p>\u003Cp>Yeah. That should be right. Collection equals conversations message dot type. What is the the type equals items? Oh, we don't really have a, maybe we wanna add a UID.\u003C/p>\u003Cp>Type equal to UID equals get combos. Alright. So now if we take a look at that, right, we should have that UID get combos. And we could just target it that way. If message dotuid equals get combos, and now we populate that data.\u003C/p>\u003Cp>Okay. Alright. So now we can see a conversation there. That was a lot of stress just to get this conversation. And let's just go in, and I'm gonna add, a message to this.\u003C/p>\u003Cp>Hi. Hey. Okay. Now if I refresh, we can see the actual messages within that. That's solid.\u003C/p>\u003Cp>Okay. And then we wanna do something like this where we have selected convo equals ref. Let's just call it conversation. Okay. Solid.\u003C/p>\u003Cp>Alright. So now we need to actually like start showing our our conversations. Right? So within this, we want to maybe wrap this up. Right?\u003C/p>\u003Cp>We'll do it in like a template tag. If there is no if no selected conversation, We're gonna show this information. Then we'll come down and do another template v if selected conversation will show something else. Alright. So instead of our actual conversations here, what if we show we wanna add a button.\u003C/p>\u003Cp>Do we want to make it a button? Yeah, we do. And then we'll do like the conversation p conversation dot title, I think we've got a title for that. Alright, so we refresh, we see test convo. Let's give each one of these a lot of padding.\u003C/p>\u003Cp>Okay. So we'll do like py6px4. You know, maybe we wrap these again. Div. Do like a divide y situation.\u003C/p>\u003Cp>Okay. Test convo. Test messages dot text. And, you know, maybe we show like the user avatar as well. Alright, so again, like we might have to drill down into this a little further and do messages dot user created, user underscore created.\u003C/p>\u003Cp>And this should give us like all the actual user data in addition to that. Right? So I wanna make sure that we've got let's see if we can get, like, the user avatar source equals, use files, conversations dot messages dot user ID. Can we show the user avatar? Where does this use files to?\u003C/p>\u003Cp>File ID. Alright, let's just check the messages real quick. Chat widget, conversations, messages. Are we getting user created? We're not getting that information, so we probably need to control that with access control as well.\u003C/p>\u003Cp>Alright. Users. Again, we're just gonna open the floodgates. Would not do this on a regular app. Make sure what other rules do we have here?\u003C/p>\u003Cp>Public. Okay. And I really don't even have a I don't think the admin user has an avatar here either. So let's just add one for that. All files, just added avatar for me.\u003C/p>\u003Cp>Refresh. Are we actually getting that avatar? We're not. Anonymous component chat widgets. We'll go in.\u003C/p>\u003Cp>Here's our conversations. Here's our messages. We'll get the user created. We could see the avatar, access control public, Directus files. Let's just give all access.\u003C/p>\u003Cp>Yep. Still not showing it. Use files. Let's just call it, like, constant file. It may not even be able to use files.\u003C/p>\u003Cp>What is actually this? Get probably need to look at the actual file URL. That's the actual method here. That's why I'm not getting that correct. Alright.\u003C/p>\u003Cp>So file URL. Okay. There we go. Alright. So that's not necessarily pretty or amazing.\u003C/p>\u003Cp>Let's flex it, test convo. Maybe we'll just show like the number of messages dot length. And wrap this. And then we do, p, maybe like get I got a getRelativeTime conversation. Oops.\u003C/p>\u003Cp>Just got to wrap that. Get relative time, conversation, date created. 16 minutes ago, we'll add a little bit of gap here. And let's do text left. Okay.\u003C/p>\u003Cp>Alright, so we got the test convo and now when I click the button at click, we want to set the selected conversation to the conversation dot ID. Okay. And now we can see that goes away. And now we just need to show the messages within that, right? So selected, let's do a conversation messages.\u003C/p>\u003Cp>Yep, get up Copilot knows what I want to do. If that is has no value, we're gonna return conversation messages. Let's see. Select a conversation dot value, conversations dot ID. Okay.\u003C/p>\u003Cp>Return conversation messages. Okay. Yeah. Sometimes AI is not to be trusted. Sometimes it's okay.\u003C/p>\u003Cp>Alright, so then let's just show the actual messages, right? Do the same thing. Let's copy this down. Div. Alright.\u003C/p>\u003Cp>And then we're gonna show we'll wrap this. And then we need a div for each message. So v for message in selected conversations, key equals message ID. Let's see what this shows us. Right?\u003C/p>\u003Cp>We've got the test conversation, selected conversation messages. Alright, so I select that. We can see this. We don't have the message text. Hey, there's the message text.\u003C/p>\u003Cp>We're going to need like some padding here. Okay. Maybe we wanna wrap each message in like a let's add just a little bit of padding. We'll wrap each message in white light gray, gray 100. And give it a little bit of padding.\u003C/p>\u003Cp>P p 2, Rounded XL. Okay. So there's our messages. How are we doing on time? We got like 17 minutes.\u003C/p>\u003Cp>We are quickly running out of time, right? So now the bottom of this, we can see the messages generated from the user here. And the other thing that I wanna do here is when we send a new message, or when we receive a message, right, if this is the, so we whenever we create new messages, it needs to be we're subscribing to the messages. Alright. So here, we're gonna need to pass the messages are received.\u003C/p>\u003Cp>We wanna add those add them to the proper conversation. Alright. So what is GitHub Copilot coming up with? We got conversations dot value dot find, message dot data dot conversation. Okay.\u003C/p>\u003Cp>If conversation dot push. All right, let's just test this out and see if we're getting what we actually want here. Local host 8055. Alright. So we got our convo there.\u003C/p>\u003Cp>I could click into it and see our test message. And now, like hopefully if I go in and add another message here and just save, We should get that, but we're not seeing that message. Test combo, yo. Why are we not receiving that? Console dot log dot message, web socket messages.\u003C/p>\u003Cp>Conversation In That should be a message of the conversations. Subscribe. Is that after maybe we need to wait getting those initial messages? All right, so let's open this up. Now we can see both of those messages, right?\u003C/p>\u003Cp>I do tttt, we're still not getting that message. So let's just drop this for now. We would come back and clean this up, but I I just wanna actually get the messages. Alright. T t t.\u003C/p>\u003Cp>Test. So we're getting that visitor messages array. It's just not populating to the proper conversation. Alright. So don't trust GitHub.\u003C/p>\u003Cp>If git combo and messages what's the actual message that we received? Git convo subscription event dot create visitor messages. It's basically we need to use that UID again. UID, those visitor messages and message data. I think that should take care of it.\u003C/p>\u003Cp>Test convo. Still not getting that. Right? Chat widget. Select a conversation messages, select a conversation, conversation.\u003C/p>\u003Cp>It's just not populating those messages here. What are we doing wrong? What are we doing wrong? If conversation oh, conversation dot value? Conversations dot value dot fun.\u003C/p>\u003Cp>That's not really gonna work, is it? So we need to like we're getting the conversation value. So here's the message. Here's the data. We're getting back an array of data actually.\u003C/p>\u003Cp>Conversations dot value dot find data. Message dot data is an array. Okay. Conversation. I don't even know if this is actually gonna work, is it?\u003C/p>\u003Cp>That shouldn't work. We'll test it and see. Yeah. It's not gonna work. Promise dot avatar.\u003C/p>\u003Cp>So we need to definitely add that as well. To our query, we're gonna add the same thing or similar, where we have fields, we're gonna have user created dot dollar sign. Okay. So we got our test convo. Can we receive messages from Reading an avatar.\u003C/p>\u003Cp>Man. Okay. Conversation dot messages dot push. Why is it not seeing the avatar? Chat widget, conversations, messages within that.\u003C/p>\u003Cp>It's pushing that into the array, message.data.0. That would be why. Alright. One more time again. I'm just gonna delete some of these.\u003C/p>\u003Cp>Save it. We refresh over here. We can see there's our convo. We do yo. We save.\u003C/p>\u003Cp>Okay. So now over here on the client, I can see that conversation or that message being populated. So we'll just add a little gap between these space space y 2. Okay. And we'll probably need to do like message dot user created dot if we have a message user dot created and message dot user created dot avatar.\u003C/p>\u003Cp>Alright, so we're only gonna show that if we have it. And then the next thing that we need to do in a hurry here, about 9 minutes, is to actually start populating these messages, right? So we want to add some form fields to this down here at the bottom somewhere. So if we're inside the selected conversation, great, let's add the message form. Cool.\u003C/p>\u003Cp>Alright, and we'll do, I've got this nice Nuxt UI library here. So we'll do Submit and Message and it just gives us some nice inputs here. Flex column, there's the wrapper. Where is this actually gonna be? Inset 0, uform, send message, v model, new message.\u003C/p>\u003Cp>Alright, so let's add our new message. New message equals ref, that'll be blank. Let's have a function for send message. Okay, send messages, create conversation, select a conversation dot value, new message dot value, user created. Nope, Where did you go?\u003C/p>\u003Cp>We can rely on AI here. And we don't want a UUID or UID for this. Oh, maybe we do. Send message. And we're we're not gonna use the user created.\u003C/p>\u003Cp>Alright. Does this give us, where's my little bar at? User, User selected conversation. There's no selected conversations. Vfusermessage.created.\u003C/p>\u003Cp>Oh, message dot user created dot avatar. Okay. So now we're going to form. The form should actually be pushed to the bottom. So that'll be flex 1 height full.\u003C/p>\u003Cp>Okay. Then inside this form, I want that to flex, justify between. Okay. And we add a bit of padding to that as well, p 2. Give it a border, border t.\u003C/p>\u003Cp>And we stick it to the bottom. Nope. This will be like overflow. Why? Auto.\u003C/p>\u003Cp>Okay. Flex justify between, and this could probably even be like stuck to the bottom in the absolute. Alright. So let's just change this to like text area. Hey hey hey.\u003C/p>\u003Cp>And now we can see we can actually chat with that specific person, inside our send message, we're probably gonna delete that new message as well. Just set that value once that's actually populating. Okay. So now I can see my convos. I can open up that convo.\u003C/p>\u003Cp>I can send a message to their team. Great. How could we actually show the help center articles? Right? In 5 minutes, can we get this rich content within there?\u003C/p>\u003Cp>We're going to be pushing that, right? Pushing it quite a bit. Alright, so we got our chat widget. We're probably gonna need like a page functionality or something. Like hey, what page are we on?\u003C/p>\u003Cp>The default is, let's set that to chat. So this will be this is our conversations. And can we wrap this in another template tag? Or it might be easier just to stick it inside a div. Alright, so div if page equals chat, We're gonna show that.\u003C/p>\u003Cp>Alright, so we should still see that now when we open this up. Still see that. Great. But let's just show another page. Div v if page equals articles.\u003C/p>\u003Cp>Alright. So then we're gonna show the articles. Maybe we show like a card v 4, Article in articles. I don't think any of these are actually gonna apply. View card, we'll just do like a p tag for the article dot title And see what this shows.\u003C/p>\u003Cp>Key is equal to article dot ID. Alright. And now we need like some kind of widget at the bottom of this to actually show, right? We need like a menu. So let's do div, do you like buttons?\u003C/p>\u003Cp>This is not at all. Show an icon. Name equals herocon's chat bubble left ellipsis. Let's just call this chat. And actually, and then we need a new button for icon name equals articles.\u003C/p>\u003Cp>How are we doing on time? 3 minutes. Are we gonna actually get this or not? Let's see what this shows. Chat.\u003C/p>\u003Cp>This is gonna be absolute bottom 0, p 4flexgap4, give that a little gap. I don't particularly like the icons for this. Let's just okay. And then if click at click, this is gonna be page equals articles. There we go.\u003C/p>\u003Cp>And at click page is equal to chat. Okay. Is that actually gonna work? Page is equal to chat. Page is equal to articles.\u003C/p>\u003Cp>Okay. So now we're switching those back and forth. We just don't have any articles, right? So let's use our client here. We've got articles.\u003C/p>\u003Cp>We could do a ref. Just make that an array. And then we'll have like a function, get articles. And we can use that same connection again. Right?\u003C/p>\u003Cp>Fetch articles. Yeah. I could use the regular rest connection as well in this case. But let's try to use this. Send message.\u003C/p>\u003Cp>We want collection of articles. This is gonna be a read. And the data, UID, we'll just do get articles. Okay. Oh, how we doing?\u003C/p>\u003Cp>We got 1 minute 20 seconds. Get articles. That's our function. Okay. And then on our receive bit here, if message dot UID and message data articles dot value dot messages.\u003C/p>\u003Cp>Okay. What else do we what else do we need to do here? We need to fetch those articles first, right? So let's do like a watcher. Watch page.\u003C/p>\u003Cp>New page. If page equals articles, get articles. Is this actually gonna work? Error. Invalid collection.\u003C/p>\u003Cp>Oh, no. We don't have articles. They're what? Post. Post.\u003C/p>\u003Cp>Post. So what is this? There we go. With what time to spare? 21 seconds.\u003C/p>\u003Cp>Right? Okay. So we can actually see a list of the articles. That's about as good as we're going to get in in this amount of time left. I'm calling that a win.\u003C/p>\u003Cp>So we can actually show the list of articles here. The next part would be to just probably actually, like, set these up to to, like, be able to read the rich text for those articles right within the little widget. But, you know, that is a wrap. Great. And if I open up Directus, we can see all the messages within that conversation.\u003C/p>\u003Cp>And honestly, I would probably build some kind of interface over here inside Directus to be able to manage this. Alright. So if we just wrap this one up, did we actually get to the intercom messenger? You know what? In an hour, almost impossible to rebuild.\u003C/p>\u003Cp>I'm I'm calling that now unless you built it before. But, yeah, far cry. But hey, the basics are there. And it goes to show you just how, how far along you can get with Directus in an hour using the tools that are provided. So that's it for this episode of 100 Apps, 100 Hours.\u003C/p>\u003Cp>Hope you enjoyed following along. You could probably see the sweat bubble up on my face at this point, but stay tuned for the next one. We'll see you.\u003C/p>","Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate here at Directus. And if you're new to the show, the basic gist is this, we take 60 minutes and we try to build or rebuild some of your favorite apps or some of your wildest app ideas, in that 60 minutes, we achieve our stated goals or we publicly fail trying. And by we, I mean me. That's okay though. When I fail, I fail spectacularly, usually. But I've got 3 little girls so I'm used to being hobbled. That's okay. There are just two rules. Number 1, you have 60 minutes to plan and build, which does pose some challenges. There's no more, no less. And number 2 is you use whatever you have at your disposal, just like in the real world. You know, make it work. Figure it out. And in today's episode, we are going to take advantage of rule number 2. So what are we building today? We are working on the intercom Messenger. Right? Intercom is a, like, help desk, onboarding tool, messaging tool. They've got all these things that are wrapped up into it. It looks like their latest positioning is, like, customer service, which is great. But like most of their it comes from this little messenger widget over here. Right? So it's not just live chat. It is kind of like a rich format where they can check product news, they can chat with the support team, there's bots, there's AI, there are articles integrated, you know, there's, like, product tours and stuff like that. The the main things that we're concerned with here are this kind of nice interface for this, and then number 2, like this actual, like, live chat functionality and maybe the ability to add articles. So we keep the user in context, That's what we're gonna be tackling today. I I think it's doable, not a 100% sure. Let's go in and start the clock on this. We will start our timer, 60 minutes and away we go. Alright. So as far as what type of functionality we want out of this, you know, what are the the jobs to be done or what are the goals for this specific project. We want to be able to chat chat with visitors in real time and then, you know, maybe display rich content inside the messenger. This could take the form of articles, help center stuff, updates, etcetera, and I totally cannot spell. How are we gonna get there though? Right? Normally, if you've watched any of the past episodes, at this point, I'm like doing some data modeling. The starting point for today is a little different in that I've already got a project that we're going to lean on. It's called Agency OS and it already has a a fully fleshed out data model. You can explore the front end of this at agencyos.dev. But this actual project you can download and test out yourself. It lives in our Directus Labs organization at, Directus dash labs slash agency dash os on GitHub. Basically, there's some instructions, but the main features of this include a website, CRM project tracker type of thing, and then you have a client portal. So on the front end, it kinda looks like this, your standard agency website. You know, we've got all these different blocks. It uses the page builder inside Directus. We got blog posts and articles. You know, there's a a help center that should be displaying some articles that doesn't. And then you have, this client portal where clients can log in, keep track of their different projects, see files, see their invoices, pay for those specific invoices. Right? This is this is pretty neat functionality. And then, you know, you would basically manage your entire workflow from your deals all the way through, like proposals, projects, and tasks inside your direct assistance. Right? So I can see a list of all the tasks that are necessary to complete this specific project, who's the organization, who are our contacts for that organization, And I can manage this whole thing here. But in this case, I'm using this because we've already got this conversations model and this messages collection. And we have things like help center collections and help center articles already mapped out, which is is really nice, just to kind of piggyback off of what I've already built in the past. So if we dive into conversations, you know, those are gonna be I can have multiple conversations ongoing at once. Those would be like the different threads. Inside that we've got like a user that created it. Maybe a visitor ID in this case, if the visitor is not known. We've got some messages inside there. Inside the messages, each message has a bit of text, who is the user or the visitor ID that created it, and away we go. That's what it looks like. We're gonna dive into like some of the real time features of Directus as well. So what have I actually got set up for you here? In starting this project, I've got a Nuxt application that I often use as a starter here for this. You know, the Nuxt application just has a direct us client using the SDK already preconfigured with, authentication and rest. I've got some login and register pages, so we can kinda simulate that. But that's kind of it. The other like 2 minutes worth of setup that I have done, this Agency OS project, before the V2 release, I had a janky little chat widget that I was trying to use in this specific project. So I can go back in time here and find this little chat widget. Let's just copy this in. And, it looks like I've already got it copied in, and cleaned up. But just a little chat box that we could use. And, I could never quite get this to work before the big release, so, it it ended up getting scrapped. So let's revisit this. I've got our chat widget. Let's drop this thing into our index page. So if we open up this incognito window so I don't spoil any of the, so I'm not using that direct to session token basically. Alright, so I've got my page here, I'm just going to drop this chat widget in there. And now I've just got this shell for a chat widget. It opens, it closes, it kinda looks nice. That's really all it does, right? There's this motionable component that uses the view motion View Use Motion, composables. Where is it? View Use Motion at view use slash motion. Just to get like some smooth animations using, I think it uses pop motion the hood. And it's just, tailwind for styling, right? Our content is gonna go in here. But let's fulfill that first thing, right? We wanna be able to chat with visitors in real time. I don't even know why these are bold, that's gonna aggravate me. Alright, how do we chat with visitors in real time? Directus has powerful real time functionality built in. Now inside my Docker Compose file for this, I've already got real time enabled. So that is the WebSockets enabled part here. And when you're working with real time and direct us, there's 3 authentication modes. There's public, handshake, and strict. Public just means anybody can open a connection. Handshake means that you don't have to provide the authentication as the you could still connect but the first message has to be authentication. And then strict means you have to pass the authentication before you actually initiate a connection. Alright, so what we're wanting to do here basically, we're gonna have to extend our plugin that we have. So I'm gonna go into my Directus plugin that I've got. And again, it's just creating a simple SDK client to work with. I'm gonna go in here and let's actually create a second client. Create Directus Real Time Client. So I'm just gonna create a new Directus client and I'll just copy paste this previous client. Great. Except with, we're not gonna use any of this, we're gonna use the real time composable. Alright? And there is an auth mode property in this case and I'm just gonna pass public because we want anybody to be able to connect to this. So I'm gonna create that client and then the next thing that I'm gonna do is provide that client to you, the Nuxt application. Let's just call it directus sws for directus web socket. And we're gonna pass directus ws. Okay. So now we have created this client. Our chat widget still doesn't do anything. That's fine. What we're gonna do is open up chat widget and we're going to access that client. So we'll do directus ws, because Nuxt provide when you use Nuxt provide it adds a dollar sign to the front of it. And we're just going to do use Nuxt app composable. That will give us access to our web socket client. And then we're going to actually open a connection, right. And this application is using like server side rendering inside Nuxt. So I probably want to like wrap this up inside a composable as well like the like an on mounted hook. So we'll await the Directus WebSocket connect. And I I don't work with Directus real time a lot, so we may get some funky behavior here that needs to be async. Okay. So I'm just gonna open up the console and see if I open up all and I go to web socket, we can show a connection here. It says switching protocols. Okay, AWS local host. Alright, so looks like we've got the WebSocket connection opened. Alright, now let's just kind of dig into the documentation a little bit. Because I the next thing is gonna be, like, subscribing, but we probably need some message handlers. So we'll go into get started with real time on the documentation. I am doing this, just as you might. So you can see that we've got the real time client here. We've opened a connection to the client, or at least I think we have. And then now we've got like this client. We need to listen for changes. Right? So I might as well just plop this in our on mounted hook. And I'm just gonna replace client dot with dollar sign directus ws. And now let's see what we actually get out of this. Do we see any actual messages here? I don't know why am I not seeing any messages. Directus cannot connect when state is open. Do we need to do we actually need to pass the auth mode to this? Is that an actual property? Switching protocols, directus.ws.subscribe. Let's subscribe to messages and see what happens. On message data dot undefined. Message. What if we just console log the message here? Console log message. Alright. Alright. So we see we're not getting anything there. Subscribe error, invalid collection. Okay. So not getting the correct collection. Let's just take a look at this. This is messages. Oh, let's try subscribing to messages, right? On open console log event open. I don't know why it is not not sure why it's not working correctly. You do not have permission to access this. Right? So as a public user, we don't have access to the individual messages. Alright, so two ways around this. We could potentially provide some authentication, if I can actually talk, right, or we could give access to this. Alright. So if I go into my access control settings, I'm gonna go to public in this case, and I'm not gonna give all access, but maybe we use custom. Right? 2 things here, we get the public can create messages, that's fine. But when it comes to reading messages, we don't want people to be able to read other people's conversations. So in this case, let's add just a rule to it. If the conversation, conversation visitor ID contains what? Can we even do that, right? We'd have to pass the let's do this instead. We'll use our website API user for this. And okay. We'll do messages. You can see conversations. I tell you what, let's just open the floodgates here just to test this out and make sure we can see these things. Alright, so we've got a subscription. There's our data, okay. And if I open this up and I add a new message, right, which is the test message, save this, we should see that data come through, which we do over here. Right? So we see our subscription for the messages. Do we see our actual data here? Great. There's our text, test message. Here's the user that created that. That's gonna be me, the admin user. And we've got the date created. Right? We've got all the details of this specific message. Alright. So at least now we are getting the messages. Alright, if we go back to the documentation, we're getting some messages here. That's great. You know, a couple things when we create a subscription, right? We can choose what event to subscribe to. So the event wise, we'll probably do like create. And we can even add a query, right. So imagine we've got a website visitor. We want to basically, like, create a unique session or a unique ID for that visitor, so that that user can only see their messages. Right? So what do we need to do there? This might be something like a Nuxt Middleware. Do I have do I have UUIDs in this? Okay. So I've just got like a function in here and and this is, like a lot of this is actually ripped from the AgisUS project to begin with. But I've just got a function here for generating an ID, which basically just creates like a UUIDv4. Alright, so what I'm gonna do here, let's just create like a session dot global dot ts. I'm gonna add this in the middleware folder inside Nuxt. And when I do this, if I use dot global that will run on every route. So I'm just gonna go here, I'm gonna copy this, and I don't really need to here. Basically, anytime we navigate to a route, we wanna do some things. Right. We want to, let's check like a session ID or visitor ID. Visitor ID equals use cookie so we can set a cookie that way we can access it server and client side. We got visitor ID. If visitor ID dot value, if there's no visitor ID dot value, we are going to set up a where is it? I'm gonna do Visitor.id.value equals generate ID. There. Okay. Alright so now basically we just created a middleware that's gonna run globally on every route within this application and it will create a cookie for the visitor ID. Okay. And now I can see that here that I've got a visitor ID cookie which is some random UUID value. If I delete that, refresh, we can see we get a new visitor ID. But that cookie will persist across sessions, which is nice, and we don't have to do anything else there. Okay, so as far as our query, if we just go back to the documentation, right, we can add a query, and we can do all of the same query params that we would in the regular Directus API. So we do something like filter where, well actually let's back up a minute, we need to get that cookie, right? So visitor ID is equal to use cookie visitor ID. Okay. And then for our filter, we want the messages. Let's think about this, right? We're gonna have to create a conversation. Let's just get some messages first, right? So we're gonna add the visitor ID to each message. Alright. So maybe we just wanna be able to see we're gonna have to like create a conversation. There's no way getting around that. Alright. How can we create items, right? So we can use that same connection and then we can actually get all of our like CRUD operations. So let's just do this. We're gonna actually set this up. I wanna subscribe to 2 items, right? Conversations. And in this case, we want to filter where the visitor ID is visitor.id.value. So inside our data model, right, when we create a conversation there's a visitor ID for it so we could track that. Great. Okay. So I could see that there. Then we have the individual messages, and we we've honestly got a lot of extra stuff that we're probably not even really gonna need here. But we got visitor. Id_id, that's what it is inside our database. And then we've got the cookie value here. So visitor dot ID, and then the param is gonna be like this, equals to visor.id.value. And then I think we can also pass a UID to this subscription. Let's look at it. Learn more about authentication, learn more about subscriptions with WebSockets. Alright. Using UID's type is subscribe multiple subscription UID. Okay. So we'll call this UID And this will be visitor conversations. Thank you, GitHub Copilot. Let's also open a connection for messages as well. Subscribe messages where messages dot create, query, UID. And in this case we can't just filter out for the visitor messages, right? We also have to be able to show the admin messages as well. So in this case what we're gonna do, we're gonna work on the conversations because we can use the related fields inside direct us collection. So I can go up a level here. It's actually conversation. Is what's the parameter gonna be? Right? So we could have potentially multiple conversations. Right? So we'll have conversations equals ref, and then we'll have messages. Actually, we might just store all of those. Right? Conversation, let's look for our query params, global query parameters for our actual filter here, filter rules, we're looking for items within an array. So we're going to use n, in conversations dot value map conversation dot ID. Okay. I think this should give us what we want. And if I open this now we can see that we've got a subscription for conversations, we've got a subscription for messages. I'm gonna just copy the visitor ID here. And the other thing I'm gonna do, I'm gonna create a new conversation. So we'll say new conversation. I'm gonna add that visitor ID here. We'll call this test convo. And do we get a message for that? We can see this conversation here. And now let's just test this as well. Will we get, TestConvo not from visitor? Just want to make sure. Okay. So you can see I created a new conversation that didn't have that user ID or that visitor ID. And now we don't actually see that. So how are we doing on time? We got about 35 minutes left. We're looking okay. So let's start having some conversations. Right? On messages, on init, Subscription init. Get conversations. Alright, subscribe on open on message. Okay, so basically all we're doing at this point is listening for messages. And just logging that out, right? So we need to do some other things here. Is there like a is there another property on a knit? No, a knit. I need more practice with real time, obviously. Alright. So when we receive the message, let's look and see what our references are here. Full code sample, I think there's actually a guide that I built on multi user chat or somebody built this. Alright. Connect cleanup, Subscribe. Okay. So you can get the actual subscription equals await client dot subscribe. Constant subscription equals ws await client.subscribe. And then we can get our initial messages. So here, let's just call this get initial combos. And we might also want to grab all the fields within that as well. So we're gonna get all the root level fields for the conversation, and then we're also gonna get all the message values included in that. And then let's populate conversations.value=subscription.data. I'm not even sure it's gonna be that. We'll just do it's actually console log subscription. See what we get there. Alright, subscription L2 generator. Cons to subscription. Subscription, unsubscribe, generator state suspended. For message of subscription, for await combo of subscription push combo. Subscription is not iterable. Let's just wrap it like in the documentation here and see what we actually get. Right? So now if I show let's just show the actual conversations. So we'll go into our content section, and we'll do divv4conversationinconversation. Flex call. Let's just add let's just log that out in conversation. Prepping the pretag. Type subscription event, init. Yeah. So this might need to actually be updated. Right? Subscription event, await client subscribe. Subscription Subscribe to messages, create event. What am I missing here? Subscribe if type. This message is create, receive message. Subscription started. Okay. We're not actually getting the message history there. Display historical messages. Okay. Alright. So basically to get all of the historical messages, we need to read the messages. So we'll do client dot, it's actually gonna be directus dot send message, and then we'll get all of the items within the conversations. So that'll just be items. The collection will be conversations. And the action is gonna be read here. So we just wanna read all of the conversations. And then as far as the query, we'll set a filter where visitor ID equals visitor ID and we'll just get all of those conversations. And maybe we limit those to like the past 10 conversations, kinda like our documentation here. Alright. Send message. Okay. So we're gonna get that message. Send message. There's our items that we could see. There's those conversations there. We could see the individual messages within those, but we're not actually populating that data somewhere. Right? So we're going to need on our message function here, basically if the data type equals items, okay, so when we receive a message, we are going to take our message. Alright. If the message dot type conversations there we go. GitHub Copilot knows what we want. And we'll just refresh when we open the connection. If I go in and we look for chat widget inside the dev tools, do we see our conversations? We don't. Alright. Why not? Message. Message, message. If let's just update this as well. Is connected. State is not open. Okay. So we're getting our messages. They're just not populating to what we want. Right? So we see the data is an array. That should be what we want though. If we open this up, we should be seeing those conversations. Direct to send messages, items, conversations, if equals conversations, conversations dot value equals messages dot data. Conversations. Yeah. That should be right. Collection equals conversations message dot type. What is the the type equals items? Oh, we don't really have a, maybe we wanna add a UID. Type equal to UID equals get combos. Alright. So now if we take a look at that, right, we should have that UID get combos. And we could just target it that way. If message dotuid equals get combos, and now we populate that data. Okay. Alright. So now we can see a conversation there. That was a lot of stress just to get this conversation. And let's just go in, and I'm gonna add, a message to this. Hi. Hey. Okay. Now if I refresh, we can see the actual messages within that. That's solid. Okay. And then we wanna do something like this where we have selected convo equals ref. Let's just call it conversation. Okay. Solid. Alright. So now we need to actually like start showing our our conversations. Right? So within this, we want to maybe wrap this up. Right? We'll do it in like a template tag. If there is no if no selected conversation, We're gonna show this information. Then we'll come down and do another template v if selected conversation will show something else. Alright. So instead of our actual conversations here, what if we show we wanna add a button. Do we want to make it a button? Yeah, we do. And then we'll do like the conversation p conversation dot title, I think we've got a title for that. Alright, so we refresh, we see test convo. Let's give each one of these a lot of padding. Okay. So we'll do like py6px4. You know, maybe we wrap these again. Div. Do like a divide y situation. Okay. Test convo. Test messages dot text. And, you know, maybe we show like the user avatar as well. Alright, so again, like we might have to drill down into this a little further and do messages dot user created, user underscore created. And this should give us like all the actual user data in addition to that. Right? So I wanna make sure that we've got let's see if we can get, like, the user avatar source equals, use files, conversations dot messages dot user ID. Can we show the user avatar? Where does this use files to? File ID. Alright, let's just check the messages real quick. Chat widget, conversations, messages. Are we getting user created? We're not getting that information, so we probably need to control that with access control as well. Alright. Users. Again, we're just gonna open the floodgates. Would not do this on a regular app. Make sure what other rules do we have here? Public. Okay. And I really don't even have a I don't think the admin user has an avatar here either. So let's just add one for that. All files, just added avatar for me. Refresh. Are we actually getting that avatar? We're not. Anonymous component chat widgets. We'll go in. Here's our conversations. Here's our messages. We'll get the user created. We could see the avatar, access control public, Directus files. Let's just give all access. Yep. Still not showing it. Use files. Let's just call it, like, constant file. It may not even be able to use files. What is actually this? Get probably need to look at the actual file URL. That's the actual method here. That's why I'm not getting that correct. Alright. So file URL. Okay. There we go. Alright. So that's not necessarily pretty or amazing. Let's flex it, test convo. Maybe we'll just show like the number of messages dot length. And wrap this. And then we do, p, maybe like get I got a getRelativeTime conversation. Oops. Just got to wrap that. Get relative time, conversation, date created. 16 minutes ago, we'll add a little bit of gap here. And let's do text left. Okay. Alright, so we got the test convo and now when I click the button at click, we want to set the selected conversation to the conversation dot ID. Okay. And now we can see that goes away. And now we just need to show the messages within that, right? So selected, let's do a conversation messages. Yep, get up Copilot knows what I want to do. If that is has no value, we're gonna return conversation messages. Let's see. Select a conversation dot value, conversations dot ID. Okay. Return conversation messages. Okay. Yeah. Sometimes AI is not to be trusted. Sometimes it's okay. Alright, so then let's just show the actual messages, right? Do the same thing. Let's copy this down. Div. Alright. And then we're gonna show we'll wrap this. And then we need a div for each message. So v for message in selected conversations, key equals message ID. Let's see what this shows us. Right? We've got the test conversation, selected conversation messages. Alright, so I select that. We can see this. We don't have the message text. Hey, there's the message text. We're going to need like some padding here. Okay. Maybe we wanna wrap each message in like a let's add just a little bit of padding. We'll wrap each message in white light gray, gray 100. And give it a little bit of padding. P p 2, Rounded XL. Okay. So there's our messages. How are we doing on time? We got like 17 minutes. We are quickly running out of time, right? So now the bottom of this, we can see the messages generated from the user here. And the other thing that I wanna do here is when we send a new message, or when we receive a message, right, if this is the, so we whenever we create new messages, it needs to be we're subscribing to the messages. Alright. So here, we're gonna need to pass the messages are received. We wanna add those add them to the proper conversation. Alright. So what is GitHub Copilot coming up with? We got conversations dot value dot find, message dot data dot conversation. Okay. If conversation dot push. All right, let's just test this out and see if we're getting what we actually want here. Local host 8055. Alright. So we got our convo there. I could click into it and see our test message. And now, like hopefully if I go in and add another message here and just save, We should get that, but we're not seeing that message. Test combo, yo. Why are we not receiving that? Console dot log dot message, web socket messages. Conversation In That should be a message of the conversations. Subscribe. Is that after maybe we need to wait getting those initial messages? All right, so let's open this up. Now we can see both of those messages, right? I do tttt, we're still not getting that message. So let's just drop this for now. We would come back and clean this up, but I I just wanna actually get the messages. Alright. T t t. Test. So we're getting that visitor messages array. It's just not populating to the proper conversation. Alright. So don't trust GitHub. If git combo and messages what's the actual message that we received? Git convo subscription event dot create visitor messages. It's basically we need to use that UID again. UID, those visitor messages and message data. I think that should take care of it. Test convo. Still not getting that. Right? Chat widget. Select a conversation messages, select a conversation, conversation. It's just not populating those messages here. What are we doing wrong? What are we doing wrong? If conversation oh, conversation dot value? Conversations dot value dot fun. That's not really gonna work, is it? So we need to like we're getting the conversation value. So here's the message. Here's the data. We're getting back an array of data actually. Conversations dot value dot find data. Message dot data is an array. Okay. Conversation. I don't even know if this is actually gonna work, is it? That shouldn't work. We'll test it and see. Yeah. It's not gonna work. Promise dot avatar. So we need to definitely add that as well. To our query, we're gonna add the same thing or similar, where we have fields, we're gonna have user created dot dollar sign. Okay. So we got our test convo. Can we receive messages from Reading an avatar. Man. Okay. Conversation dot messages dot push. Why is it not seeing the avatar? Chat widget, conversations, messages within that. It's pushing that into the array, message.data.0. That would be why. Alright. One more time again. I'm just gonna delete some of these. Save it. We refresh over here. We can see there's our convo. We do yo. We save. Okay. So now over here on the client, I can see that conversation or that message being populated. So we'll just add a little gap between these space space y 2. Okay. And we'll probably need to do like message dot user created dot if we have a message user dot created and message dot user created dot avatar. Alright, so we're only gonna show that if we have it. And then the next thing that we need to do in a hurry here, about 9 minutes, is to actually start populating these messages, right? So we want to add some form fields to this down here at the bottom somewhere. So if we're inside the selected conversation, great, let's add the message form. Cool. Alright, and we'll do, I've got this nice Nuxt UI library here. So we'll do Submit and Message and it just gives us some nice inputs here. Flex column, there's the wrapper. Where is this actually gonna be? Inset 0, uform, send message, v model, new message. Alright, so let's add our new message. New message equals ref, that'll be blank. Let's have a function for send message. Okay, send messages, create conversation, select a conversation dot value, new message dot value, user created. Nope, Where did you go? We can rely on AI here. And we don't want a UUID or UID for this. Oh, maybe we do. Send message. And we're we're not gonna use the user created. Alright. Does this give us, where's my little bar at? User, User selected conversation. There's no selected conversations. Vfusermessage.created. Oh, message dot user created dot avatar. Okay. So now we're going to form. The form should actually be pushed to the bottom. So that'll be flex 1 height full. Okay. Then inside this form, I want that to flex, justify between. Okay. And we add a bit of padding to that as well, p 2. Give it a border, border t. And we stick it to the bottom. Nope. This will be like overflow. Why? Auto. Okay. Flex justify between, and this could probably even be like stuck to the bottom in the absolute. Alright. So let's just change this to like text area. Hey hey hey. And now we can see we can actually chat with that specific person, inside our send message, we're probably gonna delete that new message as well. Just set that value once that's actually populating. Okay. So now I can see my convos. I can open up that convo. I can send a message to their team. Great. How could we actually show the help center articles? Right? In 5 minutes, can we get this rich content within there? We're going to be pushing that, right? Pushing it quite a bit. Alright, so we got our chat widget. We're probably gonna need like a page functionality or something. Like hey, what page are we on? The default is, let's set that to chat. So this will be this is our conversations. And can we wrap this in another template tag? Or it might be easier just to stick it inside a div. Alright, so div if page equals chat, We're gonna show that. Alright, so we should still see that now when we open this up. Still see that. Great. But let's just show another page. Div v if page equals articles. Alright. So then we're gonna show the articles. Maybe we show like a card v 4, Article in articles. I don't think any of these are actually gonna apply. View card, we'll just do like a p tag for the article dot title And see what this shows. Key is equal to article dot ID. Alright. And now we need like some kind of widget at the bottom of this to actually show, right? We need like a menu. So let's do div, do you like buttons? This is not at all. Show an icon. Name equals herocon's chat bubble left ellipsis. Let's just call this chat. And actually, and then we need a new button for icon name equals articles. How are we doing on time? 3 minutes. Are we gonna actually get this or not? Let's see what this shows. Chat. This is gonna be absolute bottom 0, p 4flexgap4, give that a little gap. I don't particularly like the icons for this. Let's just okay. And then if click at click, this is gonna be page equals articles. There we go. And at click page is equal to chat. Okay. Is that actually gonna work? Page is equal to chat. Page is equal to articles. Okay. So now we're switching those back and forth. We just don't have any articles, right? So let's use our client here. We've got articles. We could do a ref. Just make that an array. And then we'll have like a function, get articles. And we can use that same connection again. Right? Fetch articles. Yeah. I could use the regular rest connection as well in this case. But let's try to use this. Send message. We want collection of articles. This is gonna be a read. And the data, UID, we'll just do get articles. Okay. Oh, how we doing? We got 1 minute 20 seconds. Get articles. That's our function. Okay. And then on our receive bit here, if message dot UID and message data articles dot value dot messages. Okay. What else do we what else do we need to do here? We need to fetch those articles first, right? So let's do like a watcher. Watch page. New page. If page equals articles, get articles. Is this actually gonna work? Error. Invalid collection. Oh, no. We don't have articles. They're what? Post. Post. Post. So what is this? There we go. With what time to spare? 21 seconds. Right? Okay. So we can actually see a list of the articles. That's about as good as we're going to get in in this amount of time left. I'm calling that a win. So we can actually show the list of articles here. The next part would be to just probably actually, like, set these up to to, like, be able to read the rich text for those articles right within the little widget. But, you know, that is a wrap. Great. And if I open up Directus, we can see all the messages within that conversation. And honestly, I would probably build some kind of interface over here inside Directus to be able to manage this. Alright. So if we just wrap this one up, did we actually get to the intercom messenger? You know what? In an hour, almost impossible to rebuild. I'm I'm calling that now unless you built it before. But, yeah, far cry. But hey, the basics are there. And it goes to show you just how, how far along you can get with Directus in an hour using the tools that are provided. So that's it for this episode of 100 Apps, 100 Hours. Hope you enjoyed following along. You could probably see the sweat bubble up on my face at this point, but stay tuned for the next one. We'll see you.","7d3f53c0-2619-46ed-86c6-f7ead9eb6ea7",[515],"ceba6402-a949-47b4-8ad6-7e84252f53d9",[],{"id":157,"number":158,"show":122,"year":159,"episodes":518},[161,162,163,164,165,166,167,168,169,170],{"id":169,"slug":520,"vimeo_id":521,"description":522,"tile":523,"length":524,"resources":8,"people":8,"episode_number":341,"published":525,"title":526,"video_transcript_html":527,"video_transcript_text":528,"content":8,"seo":529,"status":130,"episode_people":530,"recommendations":532,"season":533},"social-media-platform","959732000","It’s social time. Join Bryant as he races to build a social media platform with registration, content feed and the ability to dish out likes and follows in just 60 minutes.","9aa4373b-1377-445d-acd3-e473766e6fd0",62,"2024-06-28","Mission: Social Media Platform","\u003Cp>Speaker 0: Alright. Alright. Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host Brian Gillespie, developer advocate here at Directus.\u003C/p>\n\u003Cp>And I got a doozy today. It's one, honestly, I've not been looking forward to. So if you're new to the show, basically, we rebuild or try to build some of your favorite app ideas in 60 minutes or less, or publicly fail trying. I fail a lot. But that's okay.\u003C/p>\n\u003Cp>We learn something each time. There are only two rules to this. You have 60 minutes to plan and build, which always ends up not being enough time. And rule number 2 is use whatever you have at your disposal, kind of the anti rule. So with that, let's fire into this episode, the social media platform.\u003C/p>\n\u003Cp>Again, I like I said, I've not been looking forward to this. What are we gonna be building? We're gonna try to build something similar to Twitter. Where we've got a feed, post from different people that we're gonna follow on social. And, that is what it is.\u003C/p>\n\u003Cp>So, again, it should be an interesting episode. Let's get started on the clock. Start the timer and away we go. Right? So when I look at social media platform like Twitter, you've got a list of posts, you've got users that are on the service, You can follow people.\u003C/p>\n\u003Cp>You can favorite their posts. But as far as the functionality we're really looking for out of this, let's define it right when we go to plan here. We're going to, register users, which I've already got set up, inside my example project that I use all the time in these shows. We want to follow other users, create a feed of chirps. We won't call them tweet.\u003C/p>\n\u003Cp>We'll call them chirps. Sounds great. Let's shrink this down. This is gonna be our functionality needed. Alright.\u003C/p>\n\u003Cp>That seems like a pretty good functionality set for this. Again, yeah, I can't stress how much fun I'm looking forward to having on this episode. Alright. Now as far as our data model, you know, when you kind of break it down, there's the obvious ones like users. Right?\u003C/p>\n\u003Cp>So if I do some diagramming here, we have our users. These are actually gonna come from our directus users collection that directus gives us out of the box. That gives us auth and permissions and all the fancy stuff that we're really interested in. So we'll have Directus users and then we've got the chirps. As far as the chirp, it probably has, what, like some content.\u003C/p>\n\u003Cp>Maybe there's an image or a video. You know, this could just be a file if we wanted to share that. And then a timestamp or, created at date. Right? Those are our chirps.\u003C/p>\n\u003Cp>Those have a relationship back to the user. There's a user that chirped, so we can represent that. And then, what else are we gonna have here? We're gonna have some followers. Alright.\u003C/p>\n\u003Cp>So, basically the the users are gonna follow themselves, but because that's a many to many relationship, we're gonna need like a separate table for this. So when we talk about this, we're gonna have a follower follower ID. So this is I am the follower. The followee. I always hate naming stuff.\u003C/p>\n\u003Cp>Followee ID, followedid. So again those are gonna be a two way relationship with users. And then, what else do we have? Right? The other part that I'm not a 100% sure about, we've got this feed that is unique for each user.\u003C/p>\n\u003Cp>Right? So to me that means a separate collection. So we have feeds, we've got a user that's associated to the feed. And then what else do we have? Right?\u003C/p>\n\u003Cp>We're gonna have followers, feeds. The feed is gonna be a or like updated at. When was the feed last updated? And trying to think of, like, the other paradigm here. We're gonna just have a, like, the actual tweets that are or chirps that are in that feed.\u003C/p>\n\u003Cp>Alright. And then that means we need, like, a junction table as well, feed chirps. So this will be the chirp and the feed. Alright. So I I think this is the the data model that I wanna go with for this.\u003C/p>\n\u003Cp>We'll see how this actually plays out. As far as what I've got set up, right, is the standard config that I use for all these episodes, if you've ever caught one of the past episodes. I've just got a little Nuxt starter application that has a a couple of routes, like a register and a login. Let me just clear my cookies so we can see what those look like. Right?\u003C/p>\n\u003Cp>Just a register and login route. I've got a Directus SDK plugin configured here. So we just create a REST client. There's also a real time client in case I decide that we need to use real time in this. And then on the other side of it, I've got a blank direct assistance, just a single admin user.\u003C/p>\n\u003Cp>So, alright. Let's get to work on our data model. That's what we're gonna bang out first. And like I said, we've already got direct us users here. Let's work on our chirps.\u003C/p>\n\u003Cp>So that will be the first collection that we create, and I could zoom way in inside the Directus instance here. And do I really am I concerned with the primary key field for these being UUID or auto incremented integer? You know, typically, I'm choosing UID, but in in this case, you know, maybe integer is is fine really. For created on, we're gonna do created at. And created by, we're just gonna do the user I'm gonna call that user ID.\u003C/p>\n\u003Cp>So whatever user creates this chirp, that'll be saved in the database. And then whenever they create this chirp, will be saved as well. Once the chirp is out there, it is gone. We are not going to allow editing or anything like that. And as far as our content, maybe we want to support markdown for this.\u003C/p>\n\u003Cp>Message content. Chirp content. Sounds good. And then, you know, let's get wild with it and maybe even support a file. You can upload a single file to your Chirp to share.\u003C/p>\n\u003Cp>Alright, so that's our model. I'm gonna unhide these other fields here just so I could see these when I look at them. And next I want to I'm just gonna chirp. Chirp chirp. Save it.\u003C/p>\n\u003Cp>And you'll see that saves the admin user and the date, and now we've got our first chirp for this social media platform. I don't know what we're gonna call this. Why? Why not? I think why is the example that, our CTO, Reich Van Zanten, used in one of his talks.\u003C/p>\n\u003Cp>So, alright. What's next? We're gonna do our followers. Right? So we have our followers.\u003C/p>\n\u003Cp>And in this case, we have got, really not concerned with any of those other fields. What we're gonna do, I want to go into my data model. I'm I'm gonna go to our system collection, right? And we're gonna do a Directus users, and we're gonna go in and create a many to many relationship here. It's gonna be followers.\u003C/p>\n\u003Cp>We're gonna go to the followers table. And what do we have here? We've got the directus user. This is the follower followee ID. This will be the follower ID, I think.\u003C/p>\n\u003Cp>Alright. Is that gonna work out? We got followers. We're good. And then we could add, oh, that's our where do we have that?\u003C/p>\n\u003Cp>Follow words collection. Yeah. We don't need to add that. Right? Great.\u003C/p>\n\u003Cp>And then if we delete any of these other items, we're basically just going to like if somebody if a user gets deleted or a follower gets deleted, we're just going to remove those. So okay. I hope that's gonna give us what we're looking for. So now we've got a followers table and junction direct us users followers. Oh, okay.\u003C/p>\n\u003Cp>So it's created that extra table for me. Why did it do that? Because I have a relationship there? That's some unexpected behavior, but that'd be okay. We'll roll with that.\u003C/p>\n\u003Cp>So on our users, this table is pretty much worthless, we'll just remove that. Unexpected error. Okay, fun stuff as always. Let's let's try this over again, right? We've got followers, we're just gonna delete that.\u003C/p>\n\u003Cp>Now that table is removed. We'll do another mini to mini. And we'll call it followers. And actually the related collection here we want is direct us users and not followers. So that's where I goofed up originally.\u003C/p>\n\u003Cp>And we're gonna call this followers. That'll be our new collection. We got the followee ID and then the follower ID. And I think this will get us what we want. So this will be followees.\u003C/p>\n\u003Cp>Alright. Let's try that. See if that gets us what we want. So now we have followers and followees. And then we have a table called followers that we can inspect and make sure everything is looking correct.\u003C/p>\n\u003Cp>Alright, great. So we've got that set. Now we're gonna work on our feeds, right? Whenever I want to, I like check my feed, I I need some way to store the posts that are going into that specific feed. So we'll have a feeds collection.\u003C/p>\n\u003Cp>Again this could be auto incremented. And for the feed side of it, basically we're gonna have a when was this feed updated? Let's do updated at. Just carry that same one. I I really love these optional system fields inside Directus.\u003C/p>\n\u003Cp>Again, it just gives you some of that functionality that you need like time stamps and status and things like that baked right out of the gate. Alright. So we've got a feed. We've got updated at. We've got a we're gonna have to add a user to this feed.\u003C/p>\n\u003Cp>Right? So this is a user that's gonna be a mini to 1 relationship to the user's collection, direct to users. And then if I open up the advanced settings for for any of these fields that I'm creating, I could go in and create the corresponding field. Right? So I can add additional feeds for this specific user.\u003C/p>\n\u003Cp>You know, maybe we wanted to subscribe to different topics or something like that. So on delete of directus users, we wanna delete the feeds item because there's no need for a feed for a user that is not around, right? And when it comes to our display template, I can also go in and control what this will actually display here as well. Just a nice thing if you're doing a lot of editing inside the Directus Data Studio. So we got the updated at.\u003C/p>\n\u003Cp>There's the user. And then we're gonna need another relationship between the chirps and the feed, right? So we need to know what are all the chirps within that specific feed. So, how are we gonna do this? That'll be a many to many relationship again.\u003C/p>\n\u003Cp>Because a one chirp could be in many different feeds. So we'll go here inside Directus, we'll just search for the many to many relationship, right? And we are currently where? We're on feeds. So this will be our chirps.\u003C/p>\n\u003Cp>We'll add chirps as the related collection here, we'll show a link. And I'm gonna go into the relationship under advanced settings in case I want to add this, right? This is the feed ID, this is the chirp ID. Sounds great. Looks good.\u003C/p>\n\u003Cp>And then we can add the inverse relationship that we want. So we could look at a chirp and see all the feeds that it's involved in. Not sure we necessarily need that either. As far as the sort field, we're just gonna sort by the the timing of the specific chirp. Alright.\u003C/p>\n\u003Cp>So I with that, I think we've got our data model, like, pretty fleshed out here. We got the users already. We got chirps. We got followers. We got feeds.\u003C/p>\n\u003Cp>Cool. As a user, I could go in and I could follow someone, followee, except I don't have any actual users to follow at this point, right? So if we turn our attention to the front end of the project, I can go ahead and register a new user, Right. Let's call this edonbusk@example.com. And we'll give this a very secure exam model.\u003C/p>\n\u003Cp>Okay. Alright. So if we go to log in now, we should see we're already logged in. Great. Do I see a user here?\u003C/p>\n\u003Cp>I do. Elon Busk. Should've added a first name and last name to the actual registration form. Cool. So now I've got users that I can actually follow inside this.\u003C/p>\n\u003Cp>So if I go into my admin user, I should be able to add Elon Busk as one of my followees. Man, that terminology is gonna trip me up the whole time. I should have done something else. Following or something like that. Alright, so now, we don't have a feed for this.\u003C/p>\n\u003Cp>So one of the things that we could do inside Directus, whenever a a new user is created, you probably want to add a feed for them, right? So a couple ways you could go about that. One way is just flows, right? So flows are automations inside Directus that we can create. And I could trigger these based on a number of different actions.\u003C/p>\n\u003Cp>So we're just gonna do a create profile or create feed for new users. Okay. As far as a trigger setup we're going to do an event hook. And the action we want to be is non blocking. And should be whenever we is there a way to do it?\u003C/p>\n\u003Cp>Whenever a user is created auth.create. Auth.update. So whenever items are created and I just am going to do the Directus users collection here. So we're gonna trigger this whenever a new user is created, right? If I go in and I'll just throw these up side by side.\u003C/p>\n\u003Cp>Right? I'm already logged in. Let me just destroy this. Now we'll register a new user, new user at example.com. New password.\u003C/p>\n\u003Cp>User has been created. If I refresh this we could see that. Great. We can see there's the user, there's the key that we're interested in. Alright.\u003C/p>\n\u003Cp>And now I'm just gonna go in and create a feed for this, create feed. Alright, so there's our feed. And as far as the feed data model, I'm just gonna duplicate this because I do not remember what that looked like. Hazard of the gig. Right?\u003C/p>\n\u003Cp>So if I look at the feed, we've got a user and we've got chirps. So we'll just have one param that we're gonna pass. And we could even add like a a system message if we wanted to. But for now let's just create the feed. So we'll add an operation for it, we're gonna create data, we're gonna create a feed.\u003C/p>\n\u003Cp>And then inside the payload we've got the user key. And then we're gonna dynamically pull information from the trigger for this. So trigger dot key. And we'll just use that mustache syntax. And this should get us what we want.\u003C/p>\n\u003Cp>We'll do full access. Let me just check that one more time. Trigger dot key. Okay, great. Save.\u003C/p>\n\u003Cp>And now let's try this again. Right? I see 0 feeds in here. I'm just going to remove my session token. And we'll try john@example.com.\u003C/p>\n\u003Cp>Password. So we should have a John and example here. And we can see we've got a feed for that user now as well. Great. Perfect.\u003C/p>\n\u003Cp>Alright, so if we go back to our functionality that we need, we can register users, right. Do we have the ability to follow other users? In this case, yes, we do. We don't have it wired up to the front end yet. But let's let's stay focused on Directus, right?\u003C/p>\n\u003Cp>If I am following someone, and this is not showing up really nicely. So let's just edit the interface for this. We are going to show the avatar thumbnail with a first name, last name. And I can just copy this display template here and I can do that same thing here. So the the difference between the interface and the display, the interface shows up on the form itself.\u003C/p>\n\u003Cp>So when you're in that detail view, the display shows up on the layouts. So if I'm here looking at a list of followers, this is the display and this is the actual interface, right. So let's go back into our followers and we'll we'll fix this one as well. Should just be able to copy paste the same thing. Great.\u003C/p>\n\u003Cp>Cool. Cool. There it is. We got our followers, followees. Now if I am following Elon Musk, great name by the way, anytime he chirps it should populate into my feed, right?\u003C/p>\n\u003Cp>So, if I look at my feed, I don't have a feed yet, So let's create a feed for me. And here's, here's my feed. I can't really tell which one that is, so we'll just fix that really quickly as well. We'll show a display the direct us user. Great.\u003C/p>\n\u003Cp>Cool. Alright. So now I can see there's my feed, there's John's feed. Anytime Elon Dusk tweets, I want to populate that into, a feed. Right?\u003C/p>\n\u003Cp>Or let's say if I go into John's user, for example, John Doe, maybe John Doe is following me. So if we add me as an admin user, that'll be a good example. Right? If I chirp, chirp again. Right?\u003C/p>\n\u003Cp>This is, again, this is just writing it to the SQL database. Direct is is a is a nice complement to that. Like whatever changes I'm making here being mirrored. But, I chirp. If I go to the feed, nothing happens for John Doe.\u003C/p>\n\u003Cp>Right? So whenever a new chirp comes in, I keep wanting to call it a tweet, I want to basically populate that feed for all the other users that are, following that user, right? So again, I could reach for flows on this, populate feeds. Now at the scale of something like x or or Twitter or Facebook, this is going to probably break really quickly. Because, you know, you could have millions of followers and, you're gonna want to look at other solutions to scale this.\u003C/p>\n\u003Cp>But again, this is what we can build in an hour, so let's do that. So whenever an item is created, a chirp, we're gonna do some logic on that. Right? What are we gonna do? So as far as the logic, if we just map it out.\u003C/p>\n\u003Cp>Right? New chirp. We're gonna get the user from the chirp. User from the chirp. We're going to find all the users who are following that user push chirp to their feed.\u003C/p>\n\u003Cp>Alright, seems relatively straightforward. Whenever this chirp occurs, right now that we've got this flow set up, let's just go and test new flow. Hit save. Go back to our Flows, and now populate fees. We should have a payload.\u003C/p>\n\u003Cp>Right? We've got the content. We don't see the actual user, that is in our accountability object. Right? So we need to basically get a list of all the followers and their feeds.\u003C/p>\n\u003Cp>So how are we gonna do that? We are going to get a list of feeds. So we're gonna read data. Let's do like get feeds here. We'll do from the we'll do full access.\u003C/p>\n\u003Cp>We want to basically get a list of feeds. And the feeds here, we're gonna do a filter where the, we want to dig into this, right? So if we look at our other items, if we look at our data model, right. If we're getting a feed, there are there's a user for that feed. And then we want to inside the user, we have the followees.\u003C/p>\n\u003Cp>Actually, we wanna get all the followers and then their associated feeds as well. Okay. So let's go through that route instead. We're gonna get a list of all the followers where the follow e id is equal to the dollar sign accountability dot user. And for that we're also going to get a list of fields and we want to dive in and actually go through the nesting here.\u003C/p>\n\u003Cp>So Directus allows me to basically populate all the relational data in a single API call. So we'll do the root level fields, and then again if we're going through the followers we've got a follower ID. So we'll do follower ID, that'll get us to the user, dot feeds. Is that what we want? Follower ID dot feeds.\u003C/p>\n\u003Cp>And that should give us a list of the feeds that we want to populate. Alright, let's test this out and see. Alright. So if I chirp, the expected action here for this flow it should give me like John Doe's feed ID, if I've got that set up right. Right, because John Doe has feeds here.\u003C/p>\n\u003Cp>Alright, let's test it out and see. So we'll do show up in John John's feed. Hit save. And let's go test this flow. Alright.\u003C/p>\n\u003Cp>Alright. Less than a minute ago. Let's populate. Alright. So here's all of our feeds that we've got.\u003C/p>\n\u003Cp>We can see there's the feed, there's the or those are the followers actually. And then we need to collate all these feeds. So I'm just gonna copy this over here into Versus Code. And the next thing we're gonna do, we want to push that chirp into those feeds. So we're going to update a list of feeds and push the chirp into those feeds.\u003C/p>\n\u003Cp>Alright, so how we're gonna do that? We will go in and so we are going to update the feeds. And actually we probably need a like an intermediate here. So we're gonna return let's format the feeds. Alright so if we look we are getting well we got git feeds.\u003C/p>\n\u003Cp>So that's gonna be accessible through our data object as git feeds, Alright. So here's all the feeds, that is data.gitfeeds. And in that feed where you're wanting to map through those and we're going to return an array of the feed IDs that we want to push this tweet into. Alright so feeds to update equals feeds dot map. That's gonna be our feed.\u003C/p>\n\u003Cp>And then we're going to return what the feed dot follower_id feeds the first item in that array. Feeds to update, and then we're just gonna return feeds to update. Oops, gotta spell that correct. Alright, so just a little bit of formatting logic to make this easier. And then now what we're gonna do, let's call this get feeds ID.\u003C/p>\n\u003Cp>Get feeds ID. And that should give us an array and now we're gonna update those individual feeds. So we'll do update feed, update feeds. The collection is gonna be feeds from full access. And then as far as the IDs here I'm just gonna pass this array.\u003C/p>\n\u003Cp>So get feeds, Should've put IDs at the end of that, but no worries. And then we are going to do what? Can we do the can we do the array syntax here? Create chirpid. I think we can do this.\u003C/p>\n\u003Cp>So this is gonna be in, we need chirps. So that's the field that we're gonna update within the feed. And then we're gonna use the create update syntax here. So we'll do something like this, create. So instead of like modifying the value of these chirps, alright, we're gonna create a new record inside that array, which is gonna be what?\u003C/p>\n\u003Cp>What is that gonna be exactly? So we'll go back. Chirps are in our feed. Chirps, a chirp is what, just pass the chirp ID. Alright.\u003C/p>\n\u003Cp>And the chirpid is coming from what, trigger.key. Create trigger.chirp_, actually trigger.key. Alright. Is this actually gonna do what we wanna do or not? We'll see.\u003C/p>\n\u003Cp>Alright, let's test this out, right? So now if we take a look at our feed, let's do this side by side. Alright. So we'll go back over here. How are we doing on time?\u003C/p>\n\u003Cp>I don't know if we're gonna get to the like any front end stuff on this or not. Alright. So we got our feeds. Nothing in John Doe. Now if I chirp, please show up in feed.\u003C/p>\n\u003Cp>Refresh. It's not so there's something wrong with our logic here. Let's take a look at what's going on with this. So we got our feeds, run script, syntax missing. Forgot some type of feeds.\u003C/p>\n\u003Cp>Oh, of course forgot a parenthesis, that'll always do it. Alright. So let's just test again. Test again, please. Alright.\u003C/p>\n\u003Cp>If I refresh the feed, do we have the feed? We don't see any chirps in that feed over here. So something else is going wrong. There's our invalid payload. It must be of type 1 object.\u003C/p>\n\u003Cp>Would it just be passing the ID? ID. Let's add this in. Test again. Test one more time.\u003C/p>\n\u003Cp>Save. Refresh this. Okay, okay. Did we test one more time? If I look at the feed, we've got a feed for John Doe.\u003C/p>\n\u003Cp>It's not exactly what I wanted. It's pushing a new, I guess that would be like update instead of create. Update. Let's look at the directus docs actually. Create update.\u003C/p>\n\u003Cp>That's gonna be adding items, I think. Creating multiple items. Relationships. Where is this gonna be? Global parameters.\u003C/p>\n\u003Cp>Create an item. Relational data. Okay. So assign the existing item to be a child of the current item. You can use the same structure to select what the related items are.\u003C/p>\n\u003Cp>Simply omit them from the array. So we just wanna add an item in the array. You can provide an object detailing the changes. Like we don't wanna create a blank chirp. Right?\u003C/p>\n\u003Cp>Is this what happened here? No? Why is this chirp not showing any content? So it does show up in John Doe's feed. It doesn't show the actual content, which is weird.\u003C/p>\n\u003Cp>Let's just test one more time. So what is actually happening here? This is the chirp. Oh, duh. That's what's going on.\u003C/p>\n\u003Cp>Chirp underscore ID. So we're gonna create, and it should be chirp underscore ID because we've got to go through the junction collection. Alright. So we're gonna create a new item. And this should is that gonna solve this worse?\u003C/p>\n\u003Cp>Let's see. Alright. Chirp. We'll just test this. Feeds.\u003C/p>\n\u003Cp>We hit save. Now we see there's the chirp. Baller. Okay. So now that is working as expected, right?\u003C/p>\n\u003Cp>So if I now chirp, I'm the admin user and John Doe is following me. Follow me, John. I hit save. That shirt should show up in John's actual feed. Amazing, right?\u003C/p>\n\u003Cp>It doesn't show up in anybody else's feed. Awesome. Okay. So now that logic is working as intended. So we have all that working.\u003C/p>\n\u003Cp>Create a feed of chirps. We've gotta follow other users. We've got all this functionality. Right? Let's try to put together something on the front end for this.\u003C/p>\n\u003Cp>So what is this gonna look like? I'm using Tailwind and I've got the Nuxt UI library inside this thing. Tailwind Twitter clone maybe. Search for that. Tailwind Twitter clone pages, Tailwind clone with CSS.\u003C/p>\n\u003Cp>Hey, there we go. This is probably close enough. Right? It kinda looks a mess, but let's go for it. We're just gonna copy this code.\u003C/p>\n\u003Cp>Let's create a new page. Let's just call it feed dot view. Do some script setup. Oh, that didn't give me what we want. Script setup lang equals ts.\u003C/p>\n\u003Cp>And alright so now we're gonna add this thing here. This is a giant mess of HTML. Let's just load up our user over here. Let's get logged in. Are we logged in right now?\u003C/p>\n\u003Cp>We're missing a tag somewhere. Div div. Div. Where is this thing? Oh, see it's not even a particularly great clone because there's some missing div somewhere.\u003C/p>\n\u003Cp>Great. Lots of fun. Don't ever trust these templates that you see online. Let's still where's the issue? Is it that one little div there?\u003C/p>\n\u003Cp>Where is the issue? Div. Div. Div. Div.\u003C/p>\n\u003Cp>You know what? Forget it. 1st tweet start. I don't even tired of messing with this. Alright.\u003C/p>\n\u003Cp>So let's just go back to our index page. Wasting too much time to actually get anything done here. Alright. So we're gonna have a, what, list of tweets, chirps equals, we're gonna use the await. Use async data.\u003C/p>\n\u003Cp>We're gonna give it a key. This is the user chirps, user feed. Alright. And then we're gonna do return. We're gonna import our we're actually gonna grab that from our used Nuxt composable.\u003C/p>\n\u003Cp>So used Nuxt app. Okay. We'll await the user feed. And then we're gonna import the read items from the directus SDK atdirectus SDK, and we're gonna return directus dot request read items. Wait.\u003C/p>\n\u003Cp>Use async data. Return direct us read items. Chirps. Actually, we're gonna get the feed. Where the actually, let's get all the chirps.\u003C/p>\n\u003Cp>Chirps where the feed So if this is my user, and I've got the chirps, We wanna get the feeds, actually. Get the feed for my user where the, do I have the do I have a composable in here for the user? Where is my actual user state in this application? Let's take a look at our actual state. We have a user.\u003C/p>\n\u003Cp>We have a user ID. Okay. So we're also gonna do the user equals use state user. Okay. And then we're gonna get all the chirps where the user oh, we're gonna get the feed where the user is the current user.\u003C/p>\n\u003Cp>And actually, like a a lot of this could be controlled via access control here inside Directus. So I could just see I could see anybody's chirps. I could create chirps. I can't delete any chirps. But as far as the feed, I can only see my feed.\u003C/p>\n\u003Cp>So let's just adjust this. Alright. The permissions so I can only see my feed. So the user dot ID is equal to current user dot id. Alright.\u003C/p>\n\u003Cp>So now if I just do this, honestly. Feeds, chirps. Alright. We'll refresh, failed to resolve, directus sdk@directus/sdk. Alright.\u003C/p>\n\u003Cp>So and then maybe we'll just log those out. Pre chirps. Do we see any actual chirps data? Oh, actually gonna return that. We need to deconstruct that.\u003C/p>\n\u003Cp>Chirps. Okay. So there's our feed. And we actually want to see all the chirps that are in that specific feed. Right?\u003C/p>\n\u003Cp>So now if we log in, I'm not exactly sure who I'm logged in as. And let's update our user role inside Directus to where we can at least read the item permissions. We wanna be able to read our own user. So ID equals current user. And what's that gonna give us?\u003C/p>\n\u003Cp>Refresh. Alright. So I'm not logged in. If I log in as John Doe here I don't even know what password that I gave John. Pretty sure I do know.\u003C/p>\n\u003Cp>Example. Okay. So we're gonna go back. If we refresh, do we what do we see? Not seeing anything, actually.\u003C/p>\n\u003Cp>Default invalid user credentials. If we take a look at Wait. Use async data. Data feeds. It's gonna be the feed.\u003C/p>\n\u003Cp>Let's just move this logic somewhere else. Killing me. K. Index. Just go somewhere else with this.\u003C/p>\n\u003Cp>NuxLink. Or, view button to feed to the feed Batman. Alright. So there's our feed. Why don't I see any actual chirps inside there?\u003C/p>\n\u003Cp>To the feed, Batman. And we're also not getting this stuff on the actual server side as well. So that's another thing to figure out. But our access control settings here are preventing us from seeing the actual feeds inside the the chirps inside the feed. We probably wanna be able to see followers or what as well.\u003C/p>\n\u003Cp>If we go to the feed, now, can we actually see any of this project to the feed? Just totally blow this away. Got 9 minutes left on this episode. Really struggling here to kinda sort this one out. So this is our user's feed.\u003C/p>\n\u003Cp>We should be able to see the actual chirps within that though, and I'm not sure why that is not showing up. So there's the feeds, direct us users. You know, if I were to just do something like this, local host 8055/feeds, should be like actually items slash feeds. So there I can see the actual chirps that are coming in. But is that because we got, like, caching going on here?\u003C/p>\n\u003Cp>Cash. Is there, like, a cache? User ID. Okay. So now we can see the chirps.\u003C/p>\n\u003Cp>Okay. Let's actually show the chirps themselves fields. ID, we probably want the user information, and then we'll get the chirps. Chirps. And let's just get all the the details of the chirps.\u003C/p>\n\u003Cp>Okay. So those are the the that's the junction collection. We really want the chirp dotchirp_id dash star. Okay. So now we've got a list of the chirps.\u003C/p>\n\u003Cp>Let's just iterate over that. We'll do a, dev4. What, chirpinfeed.chirps. And we can actually, like, maybe deconstruct this a little bit. Chirp underscore ID as the chirp.\u003C/p>\n\u003Cp>Key is gonna be chirp dot ID. Thank you. Then we have the chirp content. We also probably want the user ID from the chirps. We wanna know that that specific user.\u003C/p>\n\u003Cp>And in that case I'm probably gonna need to adjust my permissions as well. So access control, we'll go into users. For here, we'll just allow all access, but let's just restrict certain fields. Alright? Can't see anything except for the ones that I want.\u003C/p>\n\u003Cp>First name, last name, avatar, we don't wanna show email. We'll show ID. Followers, followee. Okay. Alright.\u003C/p>\n\u003Cp>Chirps dot underscore id.user. What are we missing? Oh, chirpsfeed.chirps. Let's just back up a minute. Feed.\u003C/p>\n\u003Cp>Pre let's throw this up here. And I'm just gonna, like, show this. Let's refresh the page, see what we got going on here To the feed. Alright. Chirps, chirp ID as the chirp in feed dot chirps.\u003C/p>\n\u003Cp>Dot content. This should work. Not sure why it's not. Right. V if feed and feed dot chirps.\u003C/p>\n\u003Cp>Feed. Oh, duh. We gotta get the first item of the feed. Transform data data dot 0. Okay.\u003C/p>\n\u003Cp>Definitely a frustrating frustrating run here. Transform data. That should give us what we need. My gosh. Return, comma.\u003C/p>\n\u003Cp>Still not. Oh, this is okay if I put this in the right spot, what a mess. Okay. I think that that should have it now, and we got a grand total of, like, 3 minutes left anyway. Do we have a feed?\u003C/p>\n\u003Cp>Yeah. Okay. So there's the chirps. Man, when we fail, we spell spectacularly sometimes. Alright.\u003C/p>\n\u003Cp>So here we go. We've got our chirps. We're showing a feed of those chirps. We've got the user underscore ID. So we refresh that.\u003C/p>\n\u003Cp>Now we could see the actual user that is chirping about that. So and we'll have a divv4. Let's do like a flex, flex call, give these some gap. We'll give each tweet some padding. We'll show a div.\u003C/p>\n\u003Cp>We'll do the username. Username chirp dot user ID or underscore user ID. Admin user. There's all of my my actual feed. Great.\u003C/p>\n\u003Cp>And, this is really really lovely. Now what if we wanted to actually chirp a bit? Right? We would create a new function, async function. Doesn't even actually make sense.\u003C/p>\n\u003Cp>We got a minute 30 left. I'm gonna go ahead and call this one a fail for today. We're gonna call this. We've got the direct us in set up really nicely on the the front end. We just really struggled a bit.\u003C/p>\n\u003Cp>You know, just an off day for me. Happens every once in a while, right? So we've got our chirps over here. The feed is is working as intended, right? If I go in and if I were to set up another follower, like if Elon Busk follows me, admin user.\u003C/p>\n\u003Cp>Save that. Obviously, like he needs a feed as well. Elon Busk. Great. And then if I chirp again, chirp for Idan.\u003C/p>\n\u003Cp>In his feed, we can see that one shows up. So there's our chirp. We can see that showing up there, which is nice. Chirp to eat on. That chirp also showed up inside John Doe's feed.\u003C/p>\n\u003Cp>So that part of it, we nailed UI. Do the explosion here. 10 seconds left. Yeah, I I think this is a good exercise from a data modeling standpoint and kind of a how to put this together. Just wasn't all there on the front end today.\u003C/p>\n\u003Cp>That's the way some of these things go. That's it for this episode of 100 apps, 100 hours. Hope you'll join me for the next one. We'll see you.\u003C/p>","Alright. Alright. Alright. Welcome back to yet another episode of 100 Apps, 100 Hours. I'm your host Brian Gillespie, developer advocate here at Directus. And I got a doozy today. It's one, honestly, I've not been looking forward to. So if you're new to the show, basically, we rebuild or try to build some of your favorite app ideas in 60 minutes or less, or publicly fail trying. I fail a lot. But that's okay. We learn something each time. There are only two rules to this. You have 60 minutes to plan and build, which always ends up not being enough time. And rule number 2 is use whatever you have at your disposal, kind of the anti rule. So with that, let's fire into this episode, the social media platform. Again, I like I said, I've not been looking forward to this. What are we gonna be building? We're gonna try to build something similar to Twitter. Where we've got a feed, post from different people that we're gonna follow on social. And, that is what it is. So, again, it should be an interesting episode. Let's get started on the clock. Start the timer and away we go. Right? So when I look at social media platform like Twitter, you've got a list of posts, you've got users that are on the service, You can follow people. You can favorite their posts. But as far as the functionality we're really looking for out of this, let's define it right when we go to plan here. We're going to, register users, which I've already got set up, inside my example project that I use all the time in these shows. We want to follow other users, create a feed of chirps. We won't call them tweet. We'll call them chirps. Sounds great. Let's shrink this down. This is gonna be our functionality needed. Alright. That seems like a pretty good functionality set for this. Again, yeah, I can't stress how much fun I'm looking forward to having on this episode. Alright. Now as far as our data model, you know, when you kind of break it down, there's the obvious ones like users. Right? So if I do some diagramming here, we have our users. These are actually gonna come from our directus users collection that directus gives us out of the box. That gives us auth and permissions and all the fancy stuff that we're really interested in. So we'll have Directus users and then we've got the chirps. As far as the chirp, it probably has, what, like some content. Maybe there's an image or a video. You know, this could just be a file if we wanted to share that. And then a timestamp or, created at date. Right? Those are our chirps. Those have a relationship back to the user. There's a user that chirped, so we can represent that. And then, what else are we gonna have here? We're gonna have some followers. Alright. So, basically the the users are gonna follow themselves, but because that's a many to many relationship, we're gonna need like a separate table for this. So when we talk about this, we're gonna have a follower follower ID. So this is I am the follower. The followee. I always hate naming stuff. Followee ID, followedid. So again those are gonna be a two way relationship with users. And then, what else do we have? Right? The other part that I'm not a 100% sure about, we've got this feed that is unique for each user. Right? So to me that means a separate collection. So we have feeds, we've got a user that's associated to the feed. And then what else do we have? Right? We're gonna have followers, feeds. The feed is gonna be a or like updated at. When was the feed last updated? And trying to think of, like, the other paradigm here. We're gonna just have a, like, the actual tweets that are or chirps that are in that feed. Alright. And then that means we need, like, a junction table as well, feed chirps. So this will be the chirp and the feed. Alright. So I I think this is the the data model that I wanna go with for this. We'll see how this actually plays out. As far as what I've got set up, right, is the standard config that I use for all these episodes, if you've ever caught one of the past episodes. I've just got a little Nuxt starter application that has a a couple of routes, like a register and a login. Let me just clear my cookies so we can see what those look like. Right? Just a register and login route. I've got a Directus SDK plugin configured here. So we just create a REST client. There's also a real time client in case I decide that we need to use real time in this. And then on the other side of it, I've got a blank direct assistance, just a single admin user. So, alright. Let's get to work on our data model. That's what we're gonna bang out first. And like I said, we've already got direct us users here. Let's work on our chirps. So that will be the first collection that we create, and I could zoom way in inside the Directus instance here. And do I really am I concerned with the primary key field for these being UUID or auto incremented integer? You know, typically, I'm choosing UID, but in in this case, you know, maybe integer is is fine really. For created on, we're gonna do created at. And created by, we're just gonna do the user I'm gonna call that user ID. So whatever user creates this chirp, that'll be saved in the database. And then whenever they create this chirp, will be saved as well. Once the chirp is out there, it is gone. We are not going to allow editing or anything like that. And as far as our content, maybe we want to support markdown for this. Message content. Chirp content. Sounds good. And then, you know, let's get wild with it and maybe even support a file. You can upload a single file to your Chirp to share. Alright, so that's our model. I'm gonna unhide these other fields here just so I could see these when I look at them. And next I want to I'm just gonna chirp. Chirp chirp. Save it. And you'll see that saves the admin user and the date, and now we've got our first chirp for this social media platform. I don't know what we're gonna call this. Why? Why not? I think why is the example that, our CTO, Reich Van Zanten, used in one of his talks. So, alright. What's next? We're gonna do our followers. Right? So we have our followers. And in this case, we have got, really not concerned with any of those other fields. What we're gonna do, I want to go into my data model. I'm I'm gonna go to our system collection, right? And we're gonna do a Directus users, and we're gonna go in and create a many to many relationship here. It's gonna be followers. We're gonna go to the followers table. And what do we have here? We've got the directus user. This is the follower followee ID. This will be the follower ID, I think. Alright. Is that gonna work out? We got followers. We're good. And then we could add, oh, that's our where do we have that? Follow words collection. Yeah. We don't need to add that. Right? Great. And then if we delete any of these other items, we're basically just going to like if somebody if a user gets deleted or a follower gets deleted, we're just going to remove those. So okay. I hope that's gonna give us what we're looking for. So now we've got a followers table and junction direct us users followers. Oh, okay. So it's created that extra table for me. Why did it do that? Because I have a relationship there? That's some unexpected behavior, but that'd be okay. We'll roll with that. So on our users, this table is pretty much worthless, we'll just remove that. Unexpected error. Okay, fun stuff as always. Let's let's try this over again, right? We've got followers, we're just gonna delete that. Now that table is removed. We'll do another mini to mini. And we'll call it followers. And actually the related collection here we want is direct us users and not followers. So that's where I goofed up originally. And we're gonna call this followers. That'll be our new collection. We got the followee ID and then the follower ID. And I think this will get us what we want. So this will be followees. Alright. Let's try that. See if that gets us what we want. So now we have followers and followees. And then we have a table called followers that we can inspect and make sure everything is looking correct. Alright, great. So we've got that set. Now we're gonna work on our feeds, right? Whenever I want to, I like check my feed, I I need some way to store the posts that are going into that specific feed. So we'll have a feeds collection. Again this could be auto incremented. And for the feed side of it, basically we're gonna have a when was this feed updated? Let's do updated at. Just carry that same one. I I really love these optional system fields inside Directus. Again, it just gives you some of that functionality that you need like time stamps and status and things like that baked right out of the gate. Alright. So we've got a feed. We've got updated at. We've got a we're gonna have to add a user to this feed. Right? So this is a user that's gonna be a mini to 1 relationship to the user's collection, direct to users. And then if I open up the advanced settings for for any of these fields that I'm creating, I could go in and create the corresponding field. Right? So I can add additional feeds for this specific user. You know, maybe we wanted to subscribe to different topics or something like that. So on delete of directus users, we wanna delete the feeds item because there's no need for a feed for a user that is not around, right? And when it comes to our display template, I can also go in and control what this will actually display here as well. Just a nice thing if you're doing a lot of editing inside the Directus Data Studio. So we got the updated at. There's the user. And then we're gonna need another relationship between the chirps and the feed, right? So we need to know what are all the chirps within that specific feed. So, how are we gonna do this? That'll be a many to many relationship again. Because a one chirp could be in many different feeds. So we'll go here inside Directus, we'll just search for the many to many relationship, right? And we are currently where? We're on feeds. So this will be our chirps. We'll add chirps as the related collection here, we'll show a link. And I'm gonna go into the relationship under advanced settings in case I want to add this, right? This is the feed ID, this is the chirp ID. Sounds great. Looks good. And then we can add the inverse relationship that we want. So we could look at a chirp and see all the feeds that it's involved in. Not sure we necessarily need that either. As far as the sort field, we're just gonna sort by the the timing of the specific chirp. Alright. So I with that, I think we've got our data model, like, pretty fleshed out here. We got the users already. We got chirps. We got followers. We got feeds. Cool. As a user, I could go in and I could follow someone, followee, except I don't have any actual users to follow at this point, right? So if we turn our attention to the front end of the project, I can go ahead and register a new user, Right. Let's call this edonbusk@example.com. And we'll give this a very secure exam model. Okay. Alright. So if we go to log in now, we should see we're already logged in. Great. Do I see a user here? I do. Elon Busk. Should've added a first name and last name to the actual registration form. Cool. So now I've got users that I can actually follow inside this. So if I go into my admin user, I should be able to add Elon Busk as one of my followees. Man, that terminology is gonna trip me up the whole time. I should have done something else. Following or something like that. Alright, so now, we don't have a feed for this. So one of the things that we could do inside Directus, whenever a a new user is created, you probably want to add a feed for them, right? So a couple ways you could go about that. One way is just flows, right? So flows are automations inside Directus that we can create. And I could trigger these based on a number of different actions. So we're just gonna do a create profile or create feed for new users. Okay. As far as a trigger setup we're going to do an event hook. And the action we want to be is non blocking. And should be whenever we is there a way to do it? Whenever a user is created auth.create. Auth.update. So whenever items are created and I just am going to do the Directus users collection here. So we're gonna trigger this whenever a new user is created, right? If I go in and I'll just throw these up side by side. Right? I'm already logged in. Let me just destroy this. Now we'll register a new user, new user at example.com. New password. User has been created. If I refresh this we could see that. Great. We can see there's the user, there's the key that we're interested in. Alright. And now I'm just gonna go in and create a feed for this, create feed. Alright, so there's our feed. And as far as the feed data model, I'm just gonna duplicate this because I do not remember what that looked like. Hazard of the gig. Right? So if I look at the feed, we've got a user and we've got chirps. So we'll just have one param that we're gonna pass. And we could even add like a a system message if we wanted to. But for now let's just create the feed. So we'll add an operation for it, we're gonna create data, we're gonna create a feed. And then inside the payload we've got the user key. And then we're gonna dynamically pull information from the trigger for this. So trigger dot key. And we'll just use that mustache syntax. And this should get us what we want. We'll do full access. Let me just check that one more time. Trigger dot key. Okay, great. Save. And now let's try this again. Right? I see 0 feeds in here. I'm just going to remove my session token. And we'll try john@example.com. Password. So we should have a John and example here. And we can see we've got a feed for that user now as well. Great. Perfect. Alright, so if we go back to our functionality that we need, we can register users, right. Do we have the ability to follow other users? In this case, yes, we do. We don't have it wired up to the front end yet. But let's let's stay focused on Directus, right? If I am following someone, and this is not showing up really nicely. So let's just edit the interface for this. We are going to show the avatar thumbnail with a first name, last name. And I can just copy this display template here and I can do that same thing here. So the the difference between the interface and the display, the interface shows up on the form itself. So when you're in that detail view, the display shows up on the layouts. So if I'm here looking at a list of followers, this is the display and this is the actual interface, right. So let's go back into our followers and we'll we'll fix this one as well. Should just be able to copy paste the same thing. Great. Cool. Cool. There it is. We got our followers, followees. Now if I am following Elon Musk, great name by the way, anytime he chirps it should populate into my feed, right? So, if I look at my feed, I don't have a feed yet, So let's create a feed for me. And here's, here's my feed. I can't really tell which one that is, so we'll just fix that really quickly as well. We'll show a display the direct us user. Great. Cool. Alright. So now I can see there's my feed, there's John's feed. Anytime Elon Dusk tweets, I want to populate that into, a feed. Right? Or let's say if I go into John's user, for example, John Doe, maybe John Doe is following me. So if we add me as an admin user, that'll be a good example. Right? If I chirp, chirp again. Right? This is, again, this is just writing it to the SQL database. Direct is is a is a nice complement to that. Like whatever changes I'm making here being mirrored. But, I chirp. If I go to the feed, nothing happens for John Doe. Right? So whenever a new chirp comes in, I keep wanting to call it a tweet, I want to basically populate that feed for all the other users that are, following that user, right? So again, I could reach for flows on this, populate feeds. Now at the scale of something like x or or Twitter or Facebook, this is going to probably break really quickly. Because, you know, you could have millions of followers and, you're gonna want to look at other solutions to scale this. But again, this is what we can build in an hour, so let's do that. So whenever an item is created, a chirp, we're gonna do some logic on that. Right? What are we gonna do? So as far as the logic, if we just map it out. Right? New chirp. We're gonna get the user from the chirp. User from the chirp. We're going to find all the users who are following that user push chirp to their feed. Alright, seems relatively straightforward. Whenever this chirp occurs, right now that we've got this flow set up, let's just go and test new flow. Hit save. Go back to our Flows, and now populate fees. We should have a payload. Right? We've got the content. We don't see the actual user, that is in our accountability object. Right? So we need to basically get a list of all the followers and their feeds. So how are we gonna do that? We are going to get a list of feeds. So we're gonna read data. Let's do like get feeds here. We'll do from the we'll do full access. We want to basically get a list of feeds. And the feeds here, we're gonna do a filter where the, we want to dig into this, right? So if we look at our other items, if we look at our data model, right. If we're getting a feed, there are there's a user for that feed. And then we want to inside the user, we have the followees. Actually, we wanna get all the followers and then their associated feeds as well. Okay. So let's go through that route instead. We're gonna get a list of all the followers where the follow e id is equal to the dollar sign accountability dot user. And for that we're also going to get a list of fields and we want to dive in and actually go through the nesting here. So Directus allows me to basically populate all the relational data in a single API call. So we'll do the root level fields, and then again if we're going through the followers we've got a follower ID. So we'll do follower ID, that'll get us to the user, dot feeds. Is that what we want? Follower ID dot feeds. And that should give us a list of the feeds that we want to populate. Alright, let's test this out and see. Alright. So if I chirp, the expected action here for this flow it should give me like John Doe's feed ID, if I've got that set up right. Right, because John Doe has feeds here. Alright, let's test it out and see. So we'll do show up in John John's feed. Hit save. And let's go test this flow. Alright. Alright. Less than a minute ago. Let's populate. Alright. So here's all of our feeds that we've got. We can see there's the feed, there's the or those are the followers actually. And then we need to collate all these feeds. So I'm just gonna copy this over here into Versus Code. And the next thing we're gonna do, we want to push that chirp into those feeds. So we're going to update a list of feeds and push the chirp into those feeds. Alright, so how we're gonna do that? We will go in and so we are going to update the feeds. And actually we probably need a like an intermediate here. So we're gonna return let's format the feeds. Alright so if we look we are getting well we got git feeds. So that's gonna be accessible through our data object as git feeds, Alright. So here's all the feeds, that is data.gitfeeds. And in that feed where you're wanting to map through those and we're going to return an array of the feed IDs that we want to push this tweet into. Alright so feeds to update equals feeds dot map. That's gonna be our feed. And then we're going to return what the feed dot follower_id feeds the first item in that array. Feeds to update, and then we're just gonna return feeds to update. Oops, gotta spell that correct. Alright, so just a little bit of formatting logic to make this easier. And then now what we're gonna do, let's call this get feeds ID. Get feeds ID. And that should give us an array and now we're gonna update those individual feeds. So we'll do update feed, update feeds. The collection is gonna be feeds from full access. And then as far as the IDs here I'm just gonna pass this array. So get feeds, Should've put IDs at the end of that, but no worries. And then we are going to do what? Can we do the can we do the array syntax here? Create chirpid. I think we can do this. So this is gonna be in, we need chirps. So that's the field that we're gonna update within the feed. And then we're gonna use the create update syntax here. So we'll do something like this, create. So instead of like modifying the value of these chirps, alright, we're gonna create a new record inside that array, which is gonna be what? What is that gonna be exactly? So we'll go back. Chirps are in our feed. Chirps, a chirp is what, just pass the chirp ID. Alright. And the chirpid is coming from what, trigger.key. Create trigger.chirp_, actually trigger.key. Alright. Is this actually gonna do what we wanna do or not? We'll see. Alright, let's test this out, right? So now if we take a look at our feed, let's do this side by side. Alright. So we'll go back over here. How are we doing on time? I don't know if we're gonna get to the like any front end stuff on this or not. Alright. So we got our feeds. Nothing in John Doe. Now if I chirp, please show up in feed. Refresh. It's not so there's something wrong with our logic here. Let's take a look at what's going on with this. So we got our feeds, run script, syntax missing. Forgot some type of feeds. Oh, of course forgot a parenthesis, that'll always do it. Alright. So let's just test again. Test again, please. Alright. If I refresh the feed, do we have the feed? We don't see any chirps in that feed over here. So something else is going wrong. There's our invalid payload. It must be of type 1 object. Would it just be passing the ID? ID. Let's add this in. Test again. Test one more time. Save. Refresh this. Okay, okay. Did we test one more time? If I look at the feed, we've got a feed for John Doe. It's not exactly what I wanted. It's pushing a new, I guess that would be like update instead of create. Update. Let's look at the directus docs actually. Create update. That's gonna be adding items, I think. Creating multiple items. Relationships. Where is this gonna be? Global parameters. Create an item. Relational data. Okay. So assign the existing item to be a child of the current item. You can use the same structure to select what the related items are. Simply omit them from the array. So we just wanna add an item in the array. You can provide an object detailing the changes. Like we don't wanna create a blank chirp. Right? Is this what happened here? No? Why is this chirp not showing any content? So it does show up in John Doe's feed. It doesn't show the actual content, which is weird. Let's just test one more time. So what is actually happening here? This is the chirp. Oh, duh. That's what's going on. Chirp underscore ID. So we're gonna create, and it should be chirp underscore ID because we've got to go through the junction collection. Alright. So we're gonna create a new item. And this should is that gonna solve this worse? Let's see. Alright. Chirp. We'll just test this. Feeds. We hit save. Now we see there's the chirp. Baller. Okay. So now that is working as expected, right? So if I now chirp, I'm the admin user and John Doe is following me. Follow me, John. I hit save. That shirt should show up in John's actual feed. Amazing, right? It doesn't show up in anybody else's feed. Awesome. Okay. So now that logic is working as intended. So we have all that working. Create a feed of chirps. We've gotta follow other users. We've got all this functionality. Right? Let's try to put together something on the front end for this. So what is this gonna look like? I'm using Tailwind and I've got the Nuxt UI library inside this thing. Tailwind Twitter clone maybe. Search for that. Tailwind Twitter clone pages, Tailwind clone with CSS. Hey, there we go. This is probably close enough. Right? It kinda looks a mess, but let's go for it. We're just gonna copy this code. Let's create a new page. Let's just call it feed dot view. Do some script setup. Oh, that didn't give me what we want. Script setup lang equals ts. And alright so now we're gonna add this thing here. This is a giant mess of HTML. Let's just load up our user over here. Let's get logged in. Are we logged in right now? We're missing a tag somewhere. Div div. Div. Where is this thing? Oh, see it's not even a particularly great clone because there's some missing div somewhere. Great. Lots of fun. Don't ever trust these templates that you see online. Let's still where's the issue? Is it that one little div there? Where is the issue? Div. Div. Div. Div. You know what? Forget it. 1st tweet start. I don't even tired of messing with this. Alright. So let's just go back to our index page. Wasting too much time to actually get anything done here. Alright. So we're gonna have a, what, list of tweets, chirps equals, we're gonna use the await. Use async data. We're gonna give it a key. This is the user chirps, user feed. Alright. And then we're gonna do return. We're gonna import our we're actually gonna grab that from our used Nuxt composable. So used Nuxt app. Okay. We'll await the user feed. And then we're gonna import the read items from the directus SDK atdirectus SDK, and we're gonna return directus dot request read items. Wait. Use async data. Return direct us read items. Chirps. Actually, we're gonna get the feed. Where the actually, let's get all the chirps. Chirps where the feed So if this is my user, and I've got the chirps, We wanna get the feeds, actually. Get the feed for my user where the, do I have the do I have a composable in here for the user? Where is my actual user state in this application? Let's take a look at our actual state. We have a user. We have a user ID. Okay. So we're also gonna do the user equals use state user. Okay. And then we're gonna get all the chirps where the user oh, we're gonna get the feed where the user is the current user. And actually, like a a lot of this could be controlled via access control here inside Directus. So I could just see I could see anybody's chirps. I could create chirps. I can't delete any chirps. But as far as the feed, I can only see my feed. So let's just adjust this. Alright. The permissions so I can only see my feed. So the user dot ID is equal to current user dot id. Alright. So now if I just do this, honestly. Feeds, chirps. Alright. We'll refresh, failed to resolve, directus sdk@directus/sdk. Alright. So and then maybe we'll just log those out. Pre chirps. Do we see any actual chirps data? Oh, actually gonna return that. We need to deconstruct that. Chirps. Okay. So there's our feed. And we actually want to see all the chirps that are in that specific feed. Right? So now if we log in, I'm not exactly sure who I'm logged in as. And let's update our user role inside Directus to where we can at least read the item permissions. We wanna be able to read our own user. So ID equals current user. And what's that gonna give us? Refresh. Alright. So I'm not logged in. If I log in as John Doe here I don't even know what password that I gave John. Pretty sure I do know. Example. Okay. So we're gonna go back. If we refresh, do we what do we see? Not seeing anything, actually. Default invalid user credentials. If we take a look at Wait. Use async data. Data feeds. It's gonna be the feed. Let's just move this logic somewhere else. Killing me. K. Index. Just go somewhere else with this. NuxLink. Or, view button to feed to the feed Batman. Alright. So there's our feed. Why don't I see any actual chirps inside there? To the feed, Batman. And we're also not getting this stuff on the actual server side as well. So that's another thing to figure out. But our access control settings here are preventing us from seeing the actual feeds inside the the chirps inside the feed. We probably wanna be able to see followers or what as well. If we go to the feed, now, can we actually see any of this project to the feed? Just totally blow this away. Got 9 minutes left on this episode. Really struggling here to kinda sort this one out. So this is our user's feed. We should be able to see the actual chirps within that though, and I'm not sure why that is not showing up. So there's the feeds, direct us users. You know, if I were to just do something like this, local host 8055/feeds, should be like actually items slash feeds. So there I can see the actual chirps that are coming in. But is that because we got, like, caching going on here? Cash. Is there, like, a cache? User ID. Okay. So now we can see the chirps. Okay. Let's actually show the chirps themselves fields. ID, we probably want the user information, and then we'll get the chirps. Chirps. And let's just get all the the details of the chirps. Okay. So those are the the that's the junction collection. We really want the chirp dotchirp_id dash star. Okay. So now we've got a list of the chirps. Let's just iterate over that. We'll do a, dev4. What, chirpinfeed.chirps. And we can actually, like, maybe deconstruct this a little bit. Chirp underscore ID as the chirp. Key is gonna be chirp dot ID. Thank you. Then we have the chirp content. We also probably want the user ID from the chirps. We wanna know that that specific user. And in that case I'm probably gonna need to adjust my permissions as well. So access control, we'll go into users. For here, we'll just allow all access, but let's just restrict certain fields. Alright? Can't see anything except for the ones that I want. First name, last name, avatar, we don't wanna show email. We'll show ID. Followers, followee. Okay. Alright. Chirps dot underscore id.user. What are we missing? Oh, chirpsfeed.chirps. Let's just back up a minute. Feed. Pre let's throw this up here. And I'm just gonna, like, show this. Let's refresh the page, see what we got going on here To the feed. Alright. Chirps, chirp ID as the chirp in feed dot chirps. Dot content. This should work. Not sure why it's not. Right. V if feed and feed dot chirps. Feed. Oh, duh. We gotta get the first item of the feed. Transform data data dot 0. Okay. Definitely a frustrating frustrating run here. Transform data. That should give us what we need. My gosh. Return, comma. Still not. Oh, this is okay if I put this in the right spot, what a mess. Okay. I think that that should have it now, and we got a grand total of, like, 3 minutes left anyway. Do we have a feed? Yeah. Okay. So there's the chirps. Man, when we fail, we spell spectacularly sometimes. Alright. So here we go. We've got our chirps. We're showing a feed of those chirps. We've got the user underscore ID. So we refresh that. Now we could see the actual user that is chirping about that. So and we'll have a divv4. Let's do like a flex, flex call, give these some gap. We'll give each tweet some padding. We'll show a div. We'll do the username. Username chirp dot user ID or underscore user ID. Admin user. There's all of my my actual feed. Great. And, this is really really lovely. Now what if we wanted to actually chirp a bit? Right? We would create a new function, async function. Doesn't even actually make sense. We got a minute 30 left. I'm gonna go ahead and call this one a fail for today. We're gonna call this. We've got the direct us in set up really nicely on the the front end. We just really struggled a bit. You know, just an off day for me. Happens every once in a while, right? So we've got our chirps over here. The feed is is working as intended, right? If I go in and if I were to set up another follower, like if Elon Busk follows me, admin user. Save that. Obviously, like he needs a feed as well. Elon Busk. Great. And then if I chirp again, chirp for Idan. In his feed, we can see that one shows up. So there's our chirp. We can see that showing up there, which is nice. Chirp to eat on. That chirp also showed up inside John Doe's feed. So that part of it, we nailed UI. Do the explosion here. 10 seconds left. Yeah, I I think this is a good exercise from a data modeling standpoint and kind of a how to put this together. Just wasn't all there on the front end today. That's the way some of these things go. That's it for this episode of 100 apps, 100 hours. Hope you'll join me for the next one. We'll see you.","00b2fc67-4c3a-47fd-9741-502f88b7231c",[531],"72dfcc94-f557-452c-b35c-0e2e84d67288",[],{"id":157,"number":158,"show":122,"year":159,"episodes":534},[161,162,163,164,165,166,167,168,169,170],{"id":170,"slug":536,"vimeo_id":537,"description":538,"tile":539,"length":524,"resources":8,"people":8,"episode_number":358,"published":540,"title":541,"video_transcript_html":542,"video_transcript_text":543,"content":8,"seo":544,"status":130,"episode_people":545,"recommendations":547,"season":548},"form-builder","988084197","Join Bryant for his final 60 minute rush of the season as he attempts to plan and build a form builder. ","23e92277-c632-4fcb-8253-71f79d984430","2024-07-25","Mission: Form Builder","\u003Cp>Speaker 0: Welcome back to another episode of 100 apps, 100 hours where we try to build or rebuild some of your favorite apps and app ideas in 60 minutes or less or die trying. Emphasis on the die trying. But, if you've not seen the show before, there are basically two rules. You have 60 minutes to plan and build, no more, no less. And the second rule, the anti rule, use whatever you have at your disposal.\u003C/p>\n\u003Cp>So today, let's get into the episode, we are going to be building a form builder slash Typeform clone. If you're watching this, you're probably familiar with Typeform, but let's just run through it really quickly. Basically, it is a form builder that, does questions. It's like 1 question at a time. You can customize the design a bit.\u003C/p>\n\u003Cp>It kinda looks like this where, hey, we fill out a form. I have to do an actual email here. Then, another question shows up. We go through this. Another question shows up.\u003C/p>\n\u003Cp>There's a lot of configurability there, but that's it really. It's a nice looking form builder that that hopefully makes people wanna fill out your forms. That's it. That's what we're gonna build. So let's dive into it.\u003C/p>\n\u003Cp>We will put where's my where's my widget? Where's my widget? Timer. Where's the time? Oh, they moved the timer on me.\u003C/p>\n\u003Cp>Okay. So we're gonna put 60 minutes on the clock. Let's roll with this. Alright. So let's start kind of planning this out.\u003C/p>\n\u003Cp>Right? I always like to start with the functionality. Feature what features do we need? Right? We need to be able to build a form, fields, field types, etcetera.\u003C/p>\n\u003Cp>How do I there we go. And then we want to be able to submit that form. Maybe render and submit form. Boom. That's it.\u003C/p>\n\u003Cp>Really? That's all the features that we need for this. We wanna build a form, maybe we call this a schema. Then we wanna render the form and submit the form on a front end. Alright.\u003C/p>\n\u003Cp>So how do we go about actually doing this? Now, there's a project that I created previously called hgos that has a kind of form builder in there. It is, not the most robust solution. So, the setup there is basically using the JSON repeater inside Directus, which is basically, just an array of form fields that we're gonna use. We pass that JSON to our component on the front end that renders a form dynamically based on the different types of inputs that we want.\u003C/p>\n\u003Cp>And then when you submit that, it all goes into a single table, into a single collection. So that is super flexible. And and if all you need to do is, output a a JSON schema to create a form and have a single collection, where you store all that data as JSON, great. Let's take this a step further in this episode though. And let's add this, make this a little more robust.\u003C/p>\n\u003Cp>So when I think about the data model, we're going to have a form or forms. Each one of those forms will probably have some form fields. Great. And then on the submission side, we've got like a form submissions. And within that, we have an ans we have answers, I guess.\u003C/p>\n\u003Cp>And so we've got the form submissions belong to the forms, the answers belong to the form submissions. Just kind of drawing these things out. There'll be a relationship between the answers and the form fields. I skipped the magic arrows here. Great.\u003C/p>\n\u003Cp>There we go. So this is kinda the way I've got it in my mind. We've got a form that holds all of the form fields. There's probably a title. We've got like a redirect or or some kind of on submit action, what we wanna do with it.\u003C/p>\n\u003Cp>On the front end, we fetch that form and all the form fields, we render it. When somebody fills out that form, we create a form submission, and that form submission is made up of answers and probably some metadata about when that form was submitted, who's submitting it, etcetera. That sounds like a good start for our planning. I'm just gonna throw these up side by side so I don't forget. And to start this out, I've got my blank instance of Directus, that's what we're using on the back end here.\u003C/p>\n\u003Cp>We're just going to hit create a new collection. Let's call this Forms. Now I'm gonna use the UUID for this. Maybe we just do the standard format here. User created, date created, date updated.\u003C/p>\n\u003Cp>I'm gonna wanna know what the status of these forms is. Do we need a sort for the forms? Maybe. Do we need a status? We'll just go ahead and add it.\u003C/p>\n\u003Cp>Alright, so let's give the form a title, maybe a form description, so that'll be a text area. And then we're going to create another collection called Form Fields. So we'll just go in, create a new collection. What's happening behind the scenes, obviously Directus is creating these tables in MySQL database. The fields, we definitely need to sort on the fields.\u003C/p>\n\u003Cp>Do we want a status on those? Maybe, maybe not. No. Let's keep status off of this. You know, do we want to know when the actual form fields were updated?\u003C/p>\n\u003Cp>Maybe I'm not super concerned with that, so we'll just leave those off. All right. So for the form fields, when we're thinking through this, right, what are we gonna need on our actual form fields? We're gonna need a name for the field. So what's the field name?\u003C/p>\n\u003Cp>We're gonna need a type for the field. So the type is probably gonna be a string, but maybe as far as, like, the interface that I wanna use, I want this to be a drop down. So we can think about input. Great. Do we have a, like, a form field for that or an icon?\u003C/p>\n\u003Cp>Yeah. Let's try that. Let's do text area. Text area. Text area.\u003C/p>\n\u003Cp>Do we have one for that? We don't. We'll just go with that option. What else do we wanna display? Like a select?\u003C/p>\n\u003Cp>Just thinking along the lines of the actual HTML form elements we're gonna use here. Drop down, maybe select. Good looking icons are sparse here. Okay. So that's probably good for now.\u003C/p>\n\u003Cp>We'll just make this a label, format each label, we don't want to show as a dot. I can actually copy and paste the choices for our interface into the display here. And this is what shows up on the, like, the index pages where you could potentially see a list of the form fields. Alright, so we've got a name, let's make this half width, make it look good when we're actually creating our form. We've got a name, I could double as label.\u003C/p>\n\u003Cp>Trying to think of what else we've got on this. Maybe some type of validation name and type. If it is a select type, we probably need, like, some available choices. Right? So let's go ahead and do that.\u003C/p>\n\u003Cp>We'll do that as the, like, JSON repeater. We'll call this select options, I guess. Great. We'll add a text field. So this is the text we're gonna display or we use kind of the label value format.\u003C/p>\n\u003Cp>So we set that up, that'll be an input, very simple. And then we have a value. In case we wanna store those differently, we can require a value, make that half width as well. Great. And let's add some conditionality to this that, if the type is select that's when we show this.\u003C/p>\n\u003Cp>So I'm gonna go into our field options, let's just hide this by default, and then inside Directus I can add conditionals to the forms inside the Studio by doing something like this. So we'll say show if type equals select. And this is just a description so I can keep my logic together. We'll say if the type equals select we're going to make sure this is not hidden, and that should give me what I am looking for. Right?\u003C/p>\n\u003Cp>We could expand this further and add, like, conditions to our front end and etcetera. What we need to do now, though, is add the relationship between those. So in this case, we want a form field. It should only belong to a single form in my mind. I I you could I guess you could make the argument either way, but in this case, let's just create that relationship.\u003C/p>\n\u003Cp>So this is gonna be a one to many relationship here. So this is gonna be called Fields, and I'm inside our forms collection, our forms table right now. I'm just going to call this key fields and we're going to look, the related collection is our form fields. And the foreign key inside the form fields table, I'm going to create a new field called form that relates back to the actual form. Gravy will show a link to the item.\u003C/p>\n\u003Cp>And if I open up this advanced field creation mode inside Directus, I get a lot more options that I can configure and play with. So I'm going to add a sort field, we're just going to make sure that sort is selected here. That allows us to drag and drop the order of those fields. And then for our triggers, like if we deselect a form field, do I want to delete that item or not? In this case I'm just gonna nullify the form field.\u003C/p>\n\u003Cp>Great, we'll display the related values. Bada bing bada boom. Now we should have a form with some fields. Let's just test this out really quickly. So we're gonna go in, we got a form.\u003C/p>\n\u003Cp>This is our contact us form, the most standard of all forms on the Internet. And here's the standard contact us form. Great. Alright, so we'll do name, is gonna be email, this will be an input. Great.\u003C/p>\n\u003Cp>What are some of the other things that we'd probably want to add to this? Like a placeholder, I would say. First name, that'll be an input. What else? Last name.\u003C/p>\n\u003Cp>Okay. Comments, that'll be maybe a text area. And now let's, let's get into the select option. Right? How can we help you?\u003C/p>\n\u003Cp>Alright. So for the options here, I need a developer, so we'll give it a label. Maybe we store the value as just developer. I need a sales rep. I don't know.\u003C/p>\n\u003Cp>Great. Okay. So a couple of things here. I don't see the actual form values that I I might look. You know, here's they're just showing IDs.\u003C/p>\n\u003Cp>How can I fix that, right? I want to make sure this looks nice when we're setting these things up. So what I can do, if I go into my form fields, we have the display template. So this is the default anytime we're referencing this form field. Or I could go into forms and go to the fields section here and I can control the display template just within forms.\u003C/p>\n\u003Cp>So that's super handy if you, need different display templates in different places, but in this case let's just go and edit the default one. Let's do the name and then we'll maybe put like a special little dot there and maybe we want to show the type of field that is. And now if we just go back to our forms, great, we can kind of see what we're working with here. I don't really let's put first name, last name, then email, then comments, and how can we help you? Maybe we move that up above.\u003C/p>\n\u003Cp>Right. Great. Alright. So now we've got the start to this form. Let's go in and actually start messing with the front end.\u003C/p>\n\u003Cp>We've got 47 minutes on the clock. I'm just gonna pull this up. Let's pull up my Nuxt application. This is just the standard Nuxt application that I have out of the box. That's my weapon of choice in this case.\u003C/p>\n\u003Cp>And let's just go in, I'm going to create a new directory in the pages directory. So this will give us a route automatically on the front end. I'm gonna call this forms, and then I'm gonna do just form in brackets here, and that will give us, like, a a dynamic form. If I do view script setup, TS setup, this is kind of what I want. Great.\u003C/p>\n\u003Cp>We've got the standard Vue Form Component. And we'll just say Form Component. Great. Alright. So let's test this out on the frontend, forms.\u003C/p>\n\u003Cp>Test. What? Uh-oh. Some type of issue going on. There it is.\u003C/p>\n\u003Cp>Form component. And this has got a layout attached to it where we're showing, like, the logo and things like that. Maybe not a huge issue for this, but, you know, the type form has, like, this full screen form effect, so, you know, maybe we dive into that later. Alright. So now I actually want to fetch that form from Directus, right?\u003C/p>\n\u003Cp>So a couple things, we probably need to edit our access control. So by default, Directus locks down every single thing inside your instance. Only admins are able to, interact with the API by default. So we're just going to go in. I'm gonna set readability for our forms and our form fields, and now if I just wanted to test that out quickly, I could do something like this in the browser where I go to my directus url/items/forms, and I can see I get the form data here, and and we'll expand that, once we dive into the Nuxt side of things.\u003C/p>\n\u003Cp>Right? So the first thing that we want to do here, we're gonna get the routes, so we know which form to fetch. And then we're gonna be using the Nuxt Async Data Call. So this Nuxt application, I've got a Directus plug in here that basically fetches information from the Directus API. We provide the SDK to the Nuxt application so I could do things like this where I just simply say give me the Directus client.\u003C/p>\n\u003Cp>And that way it's shared across the actual Nuxt application. So I get my Directus client, and then we're gonna use the, let's say data is coming back. We're gonna use the Async data call from Nuxt. And within that we are going to return Directus dot request. And then we need to import the readitem from the Directus SDK.\u003C/p>\n\u003Cp>Alright. So this is just going to read a single item. We're gonna pick that up from the actual form, from the route itself. So we'll say directives dot request. It'll be read item.\u003C/p>\n\u003Cp>We're reading items from the form collection. And then we've got route dot params dot form. Right? So that will it should give us the form. And then I can pass a a set of parameters to the Directus API to get things like form fields, etcetera.\u003C/p>\n\u003Cp>But let's just take it from here. We'll wrap this in another div. I always end up doing this just to take a look at my data and just wrap it in a pre tag. And now if I do forms test, we're probably not gonna get back any data. If I was to open this up, I don't really see anything here.\u003C/p>\n\u003Cp>Great. Alright. So now let's actually test this with the ID of one of the forms. We'll go in. Great.\u003C/p>\n\u003Cp>Okay. Looks like we are getting a cores error. So I've goos something up in my direct to settings. CORS enabled. CORS origin.\u003C/p>\n\u003Cp>Should be interesting here. Let me restart this Docker container and see if that solves my problem. If I look at my ENV, I've got HTTP local host 8055. That is that's where my direct instance is running. Okay.\u003C/p>\n\u003Cp>There we go. I just needed to set the cores in this case to make sure that local host 3,000 is allowed, and basically, I've done that the quick and dirty way just by allowing any origin here. But, typically, I would set this to, like, local host or something like that. Alright. So we could see here's our form data.\u003C/p>\n\u003Cp>That's great. How do we actually get the form fields? One of the nice things about the Direct Express API is the ability to call this data in a single API call to expand those relationships, right? So inside my form fields, maybe on the front end, I don't really care about this extra data when it was created. So let's just say we want the ID, we want the name of the form, we want the oh, it's actually the title of the form.\u003C/p>\n\u003Cp>Title. Great. And we want the description. And then we get into the fields. All right, so I could do something like this where I add a dot notation and a wildcard which will give me all the items for that field or all the all the fields within the fields collection or the the related items there basically.\u003C/p>\n\u003Cp>So instead, what I'm gonna do, if you're using the SDK, we could pass it a syntax like this where we say fields. And here, we're gonna specify the actual fields. We want the name. We want the type. We want the options, and that should give us what we're looking for here.\u003C/p>\n\u003Cp>Right? And we probably do need the ID of those fields as well. We'll look at submissions in a few. Okay, so now we've got these fields coming in, this looks good, we've got a type, we've got a name for these, we can start to construct like a form with this actual data. Now on the front end in this Nuxt application, I have this Nuxt UI library.\u003C/p>\n\u003Cp>It would be suicide to try to build this without a form library, ahead of time here or, like, some type of UI library so I could do forms. They've got this really nice setup where you can pass it a form component. They've got Zod validation, support for Yelp and Joy if you're into that. But looking at this, everything gets wrapped in this form group component, which gives you like a nice label, etcetera. So let's just try to iterate through this.\u003C/p>\n\u003Cp>Right? We've got a form. They have a u form component. Not concerned with, like, validation at this moment. Let's just display a form.\u003C/p>\n\u003Cp>Right? So we're gonna go through Uform group, v 4 fields and forms dot fields. The field, we wanna pass like a I think there's a name component for each one of these. Like, if we go in and look at the props, we gotta pass a name to it. So we'll do name.\u003C/p>\n\u003Cp>It's field dot name. Great. What else do we need here? So within the form group, then we are going to do the actual component. So we've got an input here.\u003C/p>\n\u003Cp>There's a select box. Great. What else do we have? We had text area. Right?\u003C/p>\n\u003Cp>So what I'm going to do, I'm just gonna create a map here, basically. So we'll do, a field map. And I'm just gonna do something like this where, that actually looks pretty good. You input, you text area. That's how all these are name spaced.\u003C/p>\n\u003Cp>But what I wanna do instead is use this, like, resolve component or, you know, I could even actually import these as well. So this resolve component is just a helper. You could just straight import them as well. And with Nuxt, you can actually load these dynamically as well by adding lazy in the front of them. So it only pulls in that, that component when it actually needs it dynamically.\u003C/p>\n\u003Cp>Alright. So we've got our fields, we've got our form groups within the form, and GitHub Copilot is helpful again in this aspect that we want to do the field dot type inside. We wanna pull that correct component from our map, give it a name, give it an options. Let's take a look. Options are there.\u003C/p>\n\u003Cp>Name and value. Okay. So let's save this, see what we get. It looks like we have some we got some form of something going on. I need to change this right away to input so we can actually see those.\u003C/p>\n\u003Cp>Let's give this a little bit of styling. Right? Maybe we do, like, a max width XL. Format with math width Excel MX Auto. Great.\u003C/p>\n\u003Cp>Maybe we make this a grid. We give it some gap. Why is this actually not should be parsing out my is Tailwind not included in this? What what have we got going on? My styling is not being applied from Tailwind.\u003C/p>\n\u003Cp>That's kind of upsetting. We can definitely sort that out though. And then we need a label for this as well. So we'll do the field dot name for the label. Okay, so now we've got a kinda set up in there.\u003C/p>\n\u003Cp>Let's do view button. And we'll do, like, a submit option here. Let's add that outside the form group. So we get a submit button at the bottom of this. Right?\u003C/p>\n\u003Cp>Okay. So how can we make this kind of like typeform in a way because we wanna answer these one question at a time? Or how can we kind of loop through these? Right? Gillespie.\u003C/p>\n\u003Cp>Great. We don't have a state for these, so we'll we'd need to update that as well. Let's do v model equals set up, like, our form data. Form data, maybe we use like a ref for this. It'll be form data, and we can dynamically do field dot ID.\u003C/p>\n\u003Cp>Let's put this in like an answers array. Answers. And then we're gonna have the field dot ID. Okay. Alright.\u003C/p>\n\u003Cp>And within this, I'm just gonna do answers, and we'll just preset that. All right. So now if I take a look at like my Vue Dev Tools and we go to Form, Okay. I can see my Directus object. I've got my form data.\u003C/p>\n\u003Cp>There's my answers. Now if I populate these, is that actually doing what I want? More data. Click form. Do we see our answers?\u003C/p>\n\u003Cp>Oh, maybe we want to grab the index of this as well. Field index inform dot fields. That could be helpful. Answers field index field dot id. Bryant Gillespie.\u003C/p>\n\u003Cp>Bryantdirectus.io. Let's take a look at the data we're pushing here. Okay. So I can see my fields now. Oh, nope.\u003C/p>\n\u003Cp>Form data. There's my answers. Okay. That'll that'll suffice for now. All right.\u003C/p>\n\u003Cp>So let's do these one at a time as well, right? So maybe we give this a const current field idx. We set that to 0. And then, how are we gonna want to display this? Alright.\u003C/p>\n\u003Cp>So we're gonna render each field conditionally. Alright. So let's just wrap this in a template. This will be the what are we gonna do here? So we still need to show the actual field.\u003C/p>\n\u003Cp>V. This could actually be something like this, I think. V if current field current field IDX equals field IDX template. We'll loop for these Fields. Field IDX, inform dot fields key equals field dot field.\u003C/p>\n\u003Cp>Nope. Field.id. Alright. Let's see what we've got here. Oh, don't wanna close.\u003C/p>\n\u003Cp>Let me open this up. Alright. V field dot ID. We don't need a key on that one. V4fieldsfield.\u003C/p>\n\u003Cp>Alright. So now we've got kind of this one field for each. One of the cool utilities that I use a lot is also the auto animate by a group called FormKit. It's, v autoformkit auto animate. One line of code or just a couple lines of code and you've got, like, nice form animations, for, like, list transitions, showing and hiding inputs, etcetera.\u003C/p>\n\u003Cp>And we're only gonna show the button if fields, field idx is equal to form dot fields dot length. Field.ex +1 is equal to form.fields.length. So here we shouldn't be showing that button. Give us a padding here. Cool.\u003C/p>\n\u003Cp>Alright. Now within this, we probably need, like, a previous and next button as well. So let's just do, like, a flexbox, Go to some gap. We'll do U button. Next.\u003C/p>\n\u003Cp>Okay. GitHub Copilot. Alright. So basically, if the current field IDX is greater than 0, we're going to display the previous button. If the current field IDX is not greater than the end of the form, we want to show that.\u003C/p>\n\u003Cp>But we need to fix the actual setup here. You form group. Maybe we wrap this in a div. Okay. So now you could see we could kinda go through each one of these.\u003C/p>\n\u003Cp>And do I have the actual auto enemy applied properly in this case? Via, it should be. It's not actually animating. Let me refresh. Ah, there we go.\u003C/p>\n\u003Cp>Now we're getting some nice animation or animations. Right? So we'll do some margin there. Add justification between these. And we do one of these as variant equals soft.\u003C/p>\n\u003Cp>Is that what it is? There we go. Alright, so this is the primary. Great. There we go.\u003C/p>\n\u003Cp>Cool. Alright. So now I could go through and fill out this form. Alright. Oh, that's actually submitting the form.\u003C/p>\n\u003Cp>I don't wanna do that. Alright. Next. Gillespie. May we change this to button?\u003C/p>\n\u003Cp>Great. Give him my email. Cool. Starting to look something like Typeform in this case. Alright.\u003C/p>\n\u003Cp>So test, test, test. V if field equals fields dot length. Oh, maybe this is the actual code that we need here. Why are we not seeing the so, oh, because it's not in that actual loop. There we go.\u003C/p>\n\u003Cp>So now we should see the submit button. Great. We don't have any type of handler on the submit button. And it's also not doing anything if we actually, were to submit this anyway because we've got, we we don't have anywhere to post it inside Directus. Right?\u003C/p>\n\u003Cp>We've got roughly 20 some odd minutes. Let's make this form actually submit. Right? So if we go back to our setup, right, we've got, a form submission, we've got some answers that are within that. So let's go ahead and set up those collections.\u003C/p>\n\u003Cp>Let's say form submissions. These will be DUID. This will be let's call this submitted, date submitted. That kinda matches what we've got already. Yeah.\u003C/p>\n\u003Cp>Maybe we wanna track the time that it actually user created, date updated, user updated, the status of this submission. You know, you could even like, building your own here, you could certainly, like, track partial submissions and things like that as well if you wanted to. Like, we could submit any time and then update whenever they submit the final one, sort of thing. But let's say we have, like, a date started type of thing. Maybe we wanna, like, store that whenever they start this form submission.\u003C/p>\n\u003Cp>So we could track things like how long it actually took them to do this. For that we want the date time. We're just gonna do time stamp. Cool. Day started.\u003C/p>\n\u003Cp>Date submitted. Let's make sure we show that field. I'm just going to make one of these full width so I can get what I want out of this. Okay. Alright, so the form submission, we've got to link that back to an actual form as well.\u003C/p>\n\u003Cp>So we will do a mini to 1 here. This will be our forms. And let's do forms as the related collection. Now if I open up advanced mode, I can also create the inverse relationship as well. So if I wanted to do something like this where inside forms I've got a relationship back to the submissions, which in this case makes a lot of sense because of probably want to see the submissions.\u003C/p>\n\u003Cp>We could set up that and create that relationship in a single go. So there's our form. And then the next thing that we want to do is create a form, I'm gonna call it form submission answers. It could just be form answers, I guess. Always like to, have like a a common prefix, among these things, especially when you start building projects that have a ton of tables in them.\u003C/p>\n\u003Cp>Alright. So there's probably a sort on that specific answer. Do we need any of these additional fields? Probably not, right? So when I think of the answer, we've got a relationship back to the form field.\u003C/p>\n\u003Cp>So we'll just call this field. Let's Create that. There's the form fields collection. And again, I could do the same kind of thing. And setting this up this way, you know, with all the different database tables and the collections inside Directus allows us to, you know, say I wanted to query all the fields or all the answers from a single field on the form.\u003C/p>\n\u003Cp>This makes that a lot easier as well. So if we go in here, let's just add that corresponding one to many relationship as well. Let's call this answers. Great. And then we've got, what, just like a value?\u003C/p>\n\u003Cp>I'm I'm just gonna simplify this. I'm gonna sort everything as text. You know, if you had different field types or like numbers or dates, maybe you wanna store those in the underlying database as that specific data type. You know, you could set up some different fields for that like value date, value text, value number, etcetera. But to keep this easy we're just gonna store everything as a value.\u003C/p>\n\u003Cp>Alright, so now if we just poke around, right, if I were to like manually add a submission here, I could click create. I'm not seeing the actual answers relationship here. Form answers, form submissions. Oh, that that would be why I'm not seeing it because we haven't created it yet. So the last thing that we need to do is link this form answers to the actual submissions.\u003C/p>\n\u003Cp>So we'll go in and on the form submission, this is going to be a one to many relationship, so we'll call this answers. And the submission ID, great. We'll show a link to that item. Just inspect this. It looks great.\u003C/p>\n\u003Cp>We got a sort field for those answers. Cool. Everything should be gravy. Right? So what's helpful sometimes, especially, like, if you're doing CRUD operations from the front end, maybe I wanna go into this form and I add a new submission manually.\u003C/p>\n\u003Cp>Alright. So just say this. Great. Add some answers. Here's our field that we're answering.\u003C/p>\n\u003Cp>Bryant. Create a new one. Last name, Gillespie. And this is a little bit cumbersome, but, again, we're gonna be handling this from the front end. Gmail.com.\u003C/p>\n\u003Cp>How can we help? This is gonna be what? Sales rep. And then some comments. Comments.\u003C/p>\n\u003Cp>Okay. Alright. So now we've got a submission. I can see kind of the answers here, and these are not super helpful again because we haven't adjusted like the display templates for those. So our form answers, maybe we want to show the the field name, then the actual answer.\u003C/p>\n\u003Cp>Great. Let's set that up. For our form submissions, maybe we want to show the, like, the date this was created, submitted. We wanna show the form as well. So we'll just show form dot title.\u003C/p>\n\u003Cp>And let's just edit this. Underneath the hood, this is just much syntax. We can also do dot notation to access the related fields as well. Set a little bullet point. All right, so now if we go into forms we can see the contact us form submission a minute ago.\u003C/p>\n\u003Cp>And now I can see the individual answers here, which is kind of helpful, right? But let's take a look at the structure of this on the other end. So if I wanted to see an individual submission, I'm just gonna pull this up, copy this, we've got items, form submissions. And then I can get the actual submission here. So here's kind of the structure of this, and this is what we would need to post as well, right?\u003C/p>\n\u003Cp>So we've got a form ID, then we have the individual answers. If I do question mark fields, we do just all the root level fields, we grab the answer fields, see what what kind of thing that we need to pass, right? So we got the field ID that we're gonna need to send via the API request when we submit that. And then we've got the value, so what the value of that is. Okay.\u003C/p>\n\u003Cp>So now let's take a look at actually submitting this form inside here. So let's create a new function. Let's call it Async function. We'll just call it Submit Form. Great.\u003C/p>\n\u003Cp>And what are we gonna do here? We're gonna have, like, a transform the form data to the format you need, submit to the Directus API. Alright. So if we look at our actual form data, how do we get this set up? So it's just an array of answers.\u003C/p>\n\u003Cp>I probably could have made this even simpler than that, honestly, and could have done, what, just the the form field ID. Let's just test this out. Right? Form data dot field. Let's do it this way.\u003C/p>\n\u003Cp>Answers. Form data. We still do answers. Just make that an object, and then we can add the ID for that. Async function.\u003C/p>\n\u003Cp>Oh, misspelled function. Just wondering why that wouldn't compile. Colosbybryant@gmail. Great. And now if I just refresh down here, click form, do we see our form data?\u003C/p>\n\u003Cp>There we go. We've got that. Cool. Alright. So when we submit this form, let's, transform this data.\u003C/p>\n\u003Cp>So, we're going to map through the answers. So we will do data. This is a good ID as well. We're gonna need the form ID. So this will be route dot params.form.\u003C/p>\n\u003Cp>Then we have the answers, but we're gonna need to map those first as well. Map answers. Alright. So object what do we want? Object dot values, object dot keys, form data dot answers.\u003C/p>\n\u003Cp>Yeah. There we go. For each. So we're gonna do the data key. Let's just take a look at our structure here.\u003C/p>\n\u003Cp>We've got the answers. So we need an ID. No. We don't need an ID for the actual answer. Directus should create that for us, but we do need the field ID.\u003C/p>\n\u003Cp>Alright. So we've got answers here. This is gonna be an actual array. And then for that, we're going to do answers dot push field dot key dot value. Answer is key.\u003C/p>\n\u003Cp>Great. Hey. We do console dot log just to test this out. Let's Data to be submitted. Save this.\u003C/p>\n\u003Cp>See what we got, Bryant. Oh, got all this selected. Gillespie. Okay. And then let's add submit or at click submit form.\u003C/p>\n\u003Cp>We refresh really quickly. Right. Gillespie. And now if I hit submit, Form data. Oh, that's a value.\u003C/p>\n\u003Cp>That's reactive. So let's do, like, an on that. Bryant Gillespie. Hit submit. Cannot read properties of undefined.\u003C/p>\n\u003Cp>Field uncaught in promise. Object. Keys. Answers.key. Form data answers.\u003C/p>\n\u003Cp>Console dot log, form data. Let's just unref this form data to see what we're getting. Brandt. Form data. Target.\u003C/p>\n\u003Cp>Answers for each key, data dot answers dot push, field key, form data. Oh, duh. Gotta unref that again. Silly sometimes. Great.\u003C/p>\n\u003Cp>So now I can see here's the data to be submitted. There's the field. There's my value, etcetera. Now all we should need to do is actually send this. So, let's do create item from the Directus SDK.\u003C/p>\n\u003Cp>And just the same way we can fetch those, relationships, those related fields in a single call, we can also push responses to the related fields in a single call as well. So we'll do let's do data. Let's just see it this way. I can't remember what's gonna be coming back. The async data call returns reactive values for data.\u003C/p>\n\u003Cp>But we're not gonna use async data here. This will be just like regular fetch. So we'll do await. Actually we can just use the directus SDK here as well. So we'll do directus dot request, create item, and this will be form_ submissions data.\u003C/p>\n\u003Cp>Is there anything specific we want to return from this? Nope. Let's wrap this in a try catch and just console error any errors that we receive. Form underscore submissions with the data. Console dot log response.\u003C/p>\n\u003Cp>Great. Let's see what we got. Alright. So we're just gonna pick on teammates here. We'll do a Matt Minor.\u003C/p>\n\u003Cp>Maddie atexample.com. I need a developer. Help me, please. Alright. So we hit submit.\u003C/p>\n\u003Cp>Do we see an actual network call going out here? Post forbidden. Oh, no. What are we doing wrong? Right?\u003C/p>\n\u003Cp>Where are we at on time? We got 10 minutes left. We're we're in pretty decent shape. What we don't have here is the permissions to actually create items in form answers and submissions. So we go into our public option here.\u003C/p>\n\u003Cp>We can see that, we've got the ability to read form fields and read forms, but we don't have the ability to create form submissions and create answers. Alright? So there's a couple different ways that you might wanna set this up. You know, if you wanted to, like, potentially route submissions through, directly to the Directus API or, you know, through some other, like, server side redirect, inside Nuxt through like a server route or something. Or in this case, we can just give direct public permissions to create these things.\u003C/p>\n\u003Cp>Now one of the other things that you might wanna do on the read side of it, you know, I I might want to use like a custom permission or something like that so that, people can't read all the actual form submissions. So maybe just the, IDs, etcetera. Or I could even leave this off as well. I'm just gonna enable this just while in testing. Just remember to restrict this before you go to production.\u003C/p>\n\u003Cp>So now if we test this form submission again, what do I get? I get a 200 okay. There's our submission. If I go into Directus, we can see we've got our form submission there. There's all of our answers.\u003C/p>\n\u003Cp>That's great. We don't have the date started because I didn't populate that. Right? But what if we were just to do something like this where we've got the date underscore started, And we do new date. Great.\u003C/p>\n\u003Cp>And this, let's say to ISO string. Just to be sure Directus will swallow that down. Alright. Let's test this one more time and maybe we can fix the field IDX, form field dot link, currentfieldidx. That's where we goofed up here.\u003C/p>\n\u003Cp>Okay. So now we've got Bryant. We got Gillespie. We'll just add a bunch of junk here. Email test at example.com.\u003C/p>\n\u003Cp>I need a developer. Oh, I like 35 submit buttons here. Let's go ahead and submit that down. Submit that up and see if we've got that date started field. Why do I not have that?\u003C/p>\n\u003Cp>Didn't populate. Oh, I didn't actually pass that in there. Date started. Current field, IDX. This actually needs to be just out of here, doesn't it?\u003C/p>\n\u003Cp>Alright. Bryant Gillespie test@example.com. Got a developer. Here's some comments. There's our submit.\u003C/p>\n\u003Cp>Great. Take a look at our submissions. There we go. We've got our date started. So now we could actually, like, run calculations on how long it took to fill out this form.\u003C/p>\n\u003Cp>We've got all of our answers in the correct place. We've got, like, 6 minutes left. Did we check all the boxes here? Right? We've got a form.\u003C/p>\n\u003Cp>We've got the fields. We built the form here on the back end. We've rendered and submitted the form on the front end. Amazing. Let's see if we could just take this a step further.\u003C/p>\n\u003Cp>Maybe we wanna create a dashboard for all of our form submissions. Let's see what we can get here. Maybe we do a simple metric. We want to just track the number of form submissions. So that'll be the field ID.\u003C/p>\n\u003Cp>We'll just do like a count of that ID. Total form submissions. Boom, I can see we've got 4, right? One of the other things that I like to do when building these dashboards inside Directus is make this dynamic. So what I can do is, let's say I wanted to see the answers for a specific form.\u003C/p>\n\u003Cp>What I could do is create a global relational variable. We'll call this, like, form ID. So we'll pick the form. Great. Show that out.\u003C/p>\n\u003Cp>Select a form. See what we got here. I'm just gonna move this out of the way. So we can select a form, that variable is named form ID, and then maybe I want to show a list of responses. So let's show the form submissions.\u003C/p>\n\u003Cp>And for the filter here, what I want to do, let's just show the date that that was submitted. Great. Under the filter section, what I can do here is go in and I'm gonna create a filter that uses that, that variable that we set up. So instead of hard coding a form value here, I could just do something like this where I say form underscore ID equals that variable and just wrap that in the mustache syntax. So responses for this form.\u003C/p>\n\u003Cp>Alright, so when I save this, we shouldn't see any responses here. But if I pick our contact us form, we can see those, then I can open each of those up. Or, you know, we could also change this to something like field ID. So if I wanted to see responses for a certain field, so we'll just change this from forms to a form field. We can go in, the display template for this, maybe we want to show the name of the field.\u003C/p>\n\u003Cp>Maybe we want to show the name of the form that it comes from, the title of that form, where are you. There we go. We don't need a filter there. That's gonna be our field ID. How are we doing on time?\u003C/p>\n\u003Cp>We still got 2 minutes for this. We'll change this to select a field. Field. Great. Responses for selected field.\u003C/p>\n\u003Cp>And then we'll just do the same thing here. We're just gonna change this, right? We'll change the collection to form answers. For our displayed template in this actual list, maybe we want to see the value. So we'll just show the value.\u003C/p>\n\u003Cp>And instead of this, we're gonna do the field dot ID equals field underscore ID wrapped in mustache syntax here. Alright, so now if I select First Name I can see a list of all the responses. Great. Looking great. And then maybe we even duplicate this.\u003C/p>\n\u003Cp>And let's say I wanna show those values in like a chart or something, right? So we got our donut chart here. We got our styles. The field that we want to count in that case is gonna be the value. And now I can get a breakdown of all of those responses.\u003C/p>\n\u003Cp>Great. Cool. 1 minute 24 seconds left. Ring the bell. Again, this has been a fun episode.\u003C/p>\n\u003Cp>You know, taking this from here, obviously, like, I would totally improve the styling, add some validation into it, but this would be super handy to have a form builder that you've got direct control over, that you don't have to pay outrageous prices for, well, like 30 submissions or something like that. I don't know. Let's look at the pricing. What do we have on Typeform? I like Typeform a lot.\u003C/p>\n\u003Cp>The pricing is a bit crazy. Alright. $25 a month gets you or $29 a month gets you a 100 responses. Using Directus, get way more than that for less. Amazing.\u003C/p>\n\u003Cp>Alright. That's it for this episode of 100 apps, 100 hours. I hope you've enjoyed it. I sure have. We'll catch you next time.\u003C/p>","Welcome back to another episode of 100 apps, 100 hours where we try to build or rebuild some of your favorite apps and app ideas in 60 minutes or less or die trying. Emphasis on the die trying. But, if you've not seen the show before, there are basically two rules. You have 60 minutes to plan and build, no more, no less. And the second rule, the anti rule, use whatever you have at your disposal. So today, let's get into the episode, we are going to be building a form builder slash Typeform clone. If you're watching this, you're probably familiar with Typeform, but let's just run through it really quickly. Basically, it is a form builder that, does questions. It's like 1 question at a time. You can customize the design a bit. It kinda looks like this where, hey, we fill out a form. I have to do an actual email here. Then, another question shows up. We go through this. Another question shows up. There's a lot of configurability there, but that's it really. It's a nice looking form builder that that hopefully makes people wanna fill out your forms. That's it. That's what we're gonna build. So let's dive into it. We will put where's my where's my widget? Where's my widget? Timer. Where's the time? Oh, they moved the timer on me. Okay. So we're gonna put 60 minutes on the clock. Let's roll with this. Alright. So let's start kind of planning this out. Right? I always like to start with the functionality. Feature what features do we need? Right? We need to be able to build a form, fields, field types, etcetera. How do I there we go. And then we want to be able to submit that form. Maybe render and submit form. Boom. That's it. Really? That's all the features that we need for this. We wanna build a form, maybe we call this a schema. Then we wanna render the form and submit the form on a front end. Alright. So how do we go about actually doing this? Now, there's a project that I created previously called hgos that has a kind of form builder in there. It is, not the most robust solution. So, the setup there is basically using the JSON repeater inside Directus, which is basically, just an array of form fields that we're gonna use. We pass that JSON to our component on the front end that renders a form dynamically based on the different types of inputs that we want. And then when you submit that, it all goes into a single table, into a single collection. So that is super flexible. And and if all you need to do is, output a a JSON schema to create a form and have a single collection, where you store all that data as JSON, great. Let's take this a step further in this episode though. And let's add this, make this a little more robust. So when I think about the data model, we're going to have a form or forms. Each one of those forms will probably have some form fields. Great. And then on the submission side, we've got like a form submissions. And within that, we have an ans we have answers, I guess. And so we've got the form submissions belong to the forms, the answers belong to the form submissions. Just kind of drawing these things out. There'll be a relationship between the answers and the form fields. I skipped the magic arrows here. Great. There we go. So this is kinda the way I've got it in my mind. We've got a form that holds all of the form fields. There's probably a title. We've got like a redirect or or some kind of on submit action, what we wanna do with it. On the front end, we fetch that form and all the form fields, we render it. When somebody fills out that form, we create a form submission, and that form submission is made up of answers and probably some metadata about when that form was submitted, who's submitting it, etcetera. That sounds like a good start for our planning. I'm just gonna throw these up side by side so I don't forget. And to start this out, I've got my blank instance of Directus, that's what we're using on the back end here. We're just going to hit create a new collection. Let's call this Forms. Now I'm gonna use the UUID for this. Maybe we just do the standard format here. User created, date created, date updated. I'm gonna wanna know what the status of these forms is. Do we need a sort for the forms? Maybe. Do we need a status? We'll just go ahead and add it. Alright, so let's give the form a title, maybe a form description, so that'll be a text area. And then we're going to create another collection called Form Fields. So we'll just go in, create a new collection. What's happening behind the scenes, obviously Directus is creating these tables in MySQL database. The fields, we definitely need to sort on the fields. Do we want a status on those? Maybe, maybe not. No. Let's keep status off of this. You know, do we want to know when the actual form fields were updated? Maybe I'm not super concerned with that, so we'll just leave those off. All right. So for the form fields, when we're thinking through this, right, what are we gonna need on our actual form fields? We're gonna need a name for the field. So what's the field name? We're gonna need a type for the field. So the type is probably gonna be a string, but maybe as far as, like, the interface that I wanna use, I want this to be a drop down. So we can think about input. Great. Do we have a, like, a form field for that or an icon? Yeah. Let's try that. Let's do text area. Text area. Text area. Do we have one for that? We don't. We'll just go with that option. What else do we wanna display? Like a select? Just thinking along the lines of the actual HTML form elements we're gonna use here. Drop down, maybe select. Good looking icons are sparse here. Okay. So that's probably good for now. We'll just make this a label, format each label, we don't want to show as a dot. I can actually copy and paste the choices for our interface into the display here. And this is what shows up on the, like, the index pages where you could potentially see a list of the form fields. Alright, so we've got a name, let's make this half width, make it look good when we're actually creating our form. We've got a name, I could double as label. Trying to think of what else we've got on this. Maybe some type of validation name and type. If it is a select type, we probably need, like, some available choices. Right? So let's go ahead and do that. We'll do that as the, like, JSON repeater. We'll call this select options, I guess. Great. We'll add a text field. So this is the text we're gonna display or we use kind of the label value format. So we set that up, that'll be an input, very simple. And then we have a value. In case we wanna store those differently, we can require a value, make that half width as well. Great. And let's add some conditionality to this that, if the type is select that's when we show this. So I'm gonna go into our field options, let's just hide this by default, and then inside Directus I can add conditionals to the forms inside the Studio by doing something like this. So we'll say show if type equals select. And this is just a description so I can keep my logic together. We'll say if the type equals select we're going to make sure this is not hidden, and that should give me what I am looking for. Right? We could expand this further and add, like, conditions to our front end and etcetera. What we need to do now, though, is add the relationship between those. So in this case, we want a form field. It should only belong to a single form in my mind. I I you could I guess you could make the argument either way, but in this case, let's just create that relationship. So this is gonna be a one to many relationship here. So this is gonna be called Fields, and I'm inside our forms collection, our forms table right now. I'm just going to call this key fields and we're going to look, the related collection is our form fields. And the foreign key inside the form fields table, I'm going to create a new field called form that relates back to the actual form. Gravy will show a link to the item. And if I open up this advanced field creation mode inside Directus, I get a lot more options that I can configure and play with. So I'm going to add a sort field, we're just going to make sure that sort is selected here. That allows us to drag and drop the order of those fields. And then for our triggers, like if we deselect a form field, do I want to delete that item or not? In this case I'm just gonna nullify the form field. Great, we'll display the related values. Bada bing bada boom. Now we should have a form with some fields. Let's just test this out really quickly. So we're gonna go in, we got a form. This is our contact us form, the most standard of all forms on the Internet. And here's the standard contact us form. Great. Alright, so we'll do name, is gonna be email, this will be an input. Great. What are some of the other things that we'd probably want to add to this? Like a placeholder, I would say. First name, that'll be an input. What else? Last name. Okay. Comments, that'll be maybe a text area. And now let's, let's get into the select option. Right? How can we help you? Alright. So for the options here, I need a developer, so we'll give it a label. Maybe we store the value as just developer. I need a sales rep. I don't know. Great. Okay. So a couple of things here. I don't see the actual form values that I I might look. You know, here's they're just showing IDs. How can I fix that, right? I want to make sure this looks nice when we're setting these things up. So what I can do, if I go into my form fields, we have the display template. So this is the default anytime we're referencing this form field. Or I could go into forms and go to the fields section here and I can control the display template just within forms. So that's super handy if you, need different display templates in different places, but in this case let's just go and edit the default one. Let's do the name and then we'll maybe put like a special little dot there and maybe we want to show the type of field that is. And now if we just go back to our forms, great, we can kind of see what we're working with here. I don't really let's put first name, last name, then email, then comments, and how can we help you? Maybe we move that up above. Right. Great. Alright. So now we've got the start to this form. Let's go in and actually start messing with the front end. We've got 47 minutes on the clock. I'm just gonna pull this up. Let's pull up my Nuxt application. This is just the standard Nuxt application that I have out of the box. That's my weapon of choice in this case. And let's just go in, I'm going to create a new directory in the pages directory. So this will give us a route automatically on the front end. I'm gonna call this forms, and then I'm gonna do just form in brackets here, and that will give us, like, a a dynamic form. If I do view script setup, TS setup, this is kind of what I want. Great. We've got the standard Vue Form Component. And we'll just say Form Component. Great. Alright. So let's test this out on the frontend, forms. Test. What? Uh-oh. Some type of issue going on. There it is. Form component. And this has got a layout attached to it where we're showing, like, the logo and things like that. Maybe not a huge issue for this, but, you know, the type form has, like, this full screen form effect, so, you know, maybe we dive into that later. Alright. So now I actually want to fetch that form from Directus, right? So a couple things, we probably need to edit our access control. So by default, Directus locks down every single thing inside your instance. Only admins are able to, interact with the API by default. So we're just going to go in. I'm gonna set readability for our forms and our form fields, and now if I just wanted to test that out quickly, I could do something like this in the browser where I go to my directus url/items/forms, and I can see I get the form data here, and and we'll expand that, once we dive into the Nuxt side of things. Right? So the first thing that we want to do here, we're gonna get the routes, so we know which form to fetch. And then we're gonna be using the Nuxt Async Data Call. So this Nuxt application, I've got a Directus plug in here that basically fetches information from the Directus API. We provide the SDK to the Nuxt application so I could do things like this where I just simply say give me the Directus client. And that way it's shared across the actual Nuxt application. So I get my Directus client, and then we're gonna use the, let's say data is coming back. We're gonna use the Async data call from Nuxt. And within that we are going to return Directus dot request. And then we need to import the readitem from the Directus SDK. Alright. So this is just going to read a single item. We're gonna pick that up from the actual form, from the route itself. So we'll say directives dot request. It'll be read item. We're reading items from the form collection. And then we've got route dot params dot form. Right? So that will it should give us the form. And then I can pass a a set of parameters to the Directus API to get things like form fields, etcetera. But let's just take it from here. We'll wrap this in another div. I always end up doing this just to take a look at my data and just wrap it in a pre tag. And now if I do forms test, we're probably not gonna get back any data. If I was to open this up, I don't really see anything here. Great. Alright. So now let's actually test this with the ID of one of the forms. We'll go in. Great. Okay. Looks like we are getting a cores error. So I've goos something up in my direct to settings. CORS enabled. CORS origin. Should be interesting here. Let me restart this Docker container and see if that solves my problem. If I look at my ENV, I've got HTTP local host 8055. That is that's where my direct instance is running. Okay. There we go. I just needed to set the cores in this case to make sure that local host 3,000 is allowed, and basically, I've done that the quick and dirty way just by allowing any origin here. But, typically, I would set this to, like, local host or something like that. Alright. So we could see here's our form data. That's great. How do we actually get the form fields? One of the nice things about the Direct Express API is the ability to call this data in a single API call to expand those relationships, right? So inside my form fields, maybe on the front end, I don't really care about this extra data when it was created. So let's just say we want the ID, we want the name of the form, we want the oh, it's actually the title of the form. Title. Great. And we want the description. And then we get into the fields. All right, so I could do something like this where I add a dot notation and a wildcard which will give me all the items for that field or all the all the fields within the fields collection or the the related items there basically. So instead, what I'm gonna do, if you're using the SDK, we could pass it a syntax like this where we say fields. And here, we're gonna specify the actual fields. We want the name. We want the type. We want the options, and that should give us what we're looking for here. Right? And we probably do need the ID of those fields as well. We'll look at submissions in a few. Okay, so now we've got these fields coming in, this looks good, we've got a type, we've got a name for these, we can start to construct like a form with this actual data. Now on the front end in this Nuxt application, I have this Nuxt UI library. It would be suicide to try to build this without a form library, ahead of time here or, like, some type of UI library so I could do forms. They've got this really nice setup where you can pass it a form component. They've got Zod validation, support for Yelp and Joy if you're into that. But looking at this, everything gets wrapped in this form group component, which gives you like a nice label, etcetera. So let's just try to iterate through this. Right? We've got a form. They have a u form component. Not concerned with, like, validation at this moment. Let's just display a form. Right? So we're gonna go through Uform group, v 4 fields and forms dot fields. The field, we wanna pass like a I think there's a name component for each one of these. Like, if we go in and look at the props, we gotta pass a name to it. So we'll do name. It's field dot name. Great. What else do we need here? So within the form group, then we are going to do the actual component. So we've got an input here. There's a select box. Great. What else do we have? We had text area. Right? So what I'm going to do, I'm just gonna create a map here, basically. So we'll do, a field map. And I'm just gonna do something like this where, that actually looks pretty good. You input, you text area. That's how all these are name spaced. But what I wanna do instead is use this, like, resolve component or, you know, I could even actually import these as well. So this resolve component is just a helper. You could just straight import them as well. And with Nuxt, you can actually load these dynamically as well by adding lazy in the front of them. So it only pulls in that, that component when it actually needs it dynamically. Alright. So we've got our fields, we've got our form groups within the form, and GitHub Copilot is helpful again in this aspect that we want to do the field dot type inside. We wanna pull that correct component from our map, give it a name, give it an options. Let's take a look. Options are there. Name and value. Okay. So let's save this, see what we get. It looks like we have some we got some form of something going on. I need to change this right away to input so we can actually see those. Let's give this a little bit of styling. Right? Maybe we do, like, a max width XL. Format with math width Excel MX Auto. Great. Maybe we make this a grid. We give it some gap. Why is this actually not should be parsing out my is Tailwind not included in this? What what have we got going on? My styling is not being applied from Tailwind. That's kind of upsetting. We can definitely sort that out though. And then we need a label for this as well. So we'll do the field dot name for the label. Okay, so now we've got a kinda set up in there. Let's do view button. And we'll do, like, a submit option here. Let's add that outside the form group. So we get a submit button at the bottom of this. Right? Okay. So how can we make this kind of like typeform in a way because we wanna answer these one question at a time? Or how can we kind of loop through these? Right? Gillespie. Great. We don't have a state for these, so we'll we'd need to update that as well. Let's do v model equals set up, like, our form data. Form data, maybe we use like a ref for this. It'll be form data, and we can dynamically do field dot ID. Let's put this in like an answers array. Answers. And then we're gonna have the field dot ID. Okay. Alright. And within this, I'm just gonna do answers, and we'll just preset that. All right. So now if I take a look at like my Vue Dev Tools and we go to Form, Okay. I can see my Directus object. I've got my form data. There's my answers. Now if I populate these, is that actually doing what I want? More data. Click form. Do we see our answers? Oh, maybe we want to grab the index of this as well. Field index inform dot fields. That could be helpful. Answers field index field dot id. Bryant Gillespie. Bryantdirectus.io. Let's take a look at the data we're pushing here. Okay. So I can see my fields now. Oh, nope. Form data. There's my answers. Okay. That'll that'll suffice for now. All right. So let's do these one at a time as well, right? So maybe we give this a const current field idx. We set that to 0. And then, how are we gonna want to display this? Alright. So we're gonna render each field conditionally. Alright. So let's just wrap this in a template. This will be the what are we gonna do here? So we still need to show the actual field. V. This could actually be something like this, I think. V if current field current field IDX equals field IDX template. We'll loop for these Fields. Field IDX, inform dot fields key equals field dot field. Nope. Field.id. Alright. Let's see what we've got here. Oh, don't wanna close. Let me open this up. Alright. V field dot ID. We don't need a key on that one. V4fieldsfield. Alright. So now we've got kind of this one field for each. One of the cool utilities that I use a lot is also the auto animate by a group called FormKit. It's, v autoformkit auto animate. One line of code or just a couple lines of code and you've got, like, nice form animations, for, like, list transitions, showing and hiding inputs, etcetera. And we're only gonna show the button if fields, field idx is equal to form dot fields dot length. Field.ex +1 is equal to form.fields.length. So here we shouldn't be showing that button. Give us a padding here. Cool. Alright. Now within this, we probably need, like, a previous and next button as well. So let's just do, like, a flexbox, Go to some gap. We'll do U button. Next. Okay. GitHub Copilot. Alright. So basically, if the current field IDX is greater than 0, we're going to display the previous button. If the current field IDX is not greater than the end of the form, we want to show that. But we need to fix the actual setup here. You form group. Maybe we wrap this in a div. Okay. So now you could see we could kinda go through each one of these. And do I have the actual auto enemy applied properly in this case? Via, it should be. It's not actually animating. Let me refresh. Ah, there we go. Now we're getting some nice animation or animations. Right? So we'll do some margin there. Add justification between these. And we do one of these as variant equals soft. Is that what it is? There we go. Alright, so this is the primary. Great. There we go. Cool. Alright. So now I could go through and fill out this form. Alright. Oh, that's actually submitting the form. I don't wanna do that. Alright. Next. Gillespie. May we change this to button? Great. Give him my email. Cool. Starting to look something like Typeform in this case. Alright. So test, test, test. V if field equals fields dot length. Oh, maybe this is the actual code that we need here. Why are we not seeing the so, oh, because it's not in that actual loop. There we go. So now we should see the submit button. Great. We don't have any type of handler on the submit button. And it's also not doing anything if we actually, were to submit this anyway because we've got, we we don't have anywhere to post it inside Directus. Right? We've got roughly 20 some odd minutes. Let's make this form actually submit. Right? So if we go back to our setup, right, we've got, a form submission, we've got some answers that are within that. So let's go ahead and set up those collections. Let's say form submissions. These will be DUID. This will be let's call this submitted, date submitted. That kinda matches what we've got already. Yeah. Maybe we wanna track the time that it actually user created, date updated, user updated, the status of this submission. You know, you could even like, building your own here, you could certainly, like, track partial submissions and things like that as well if you wanted to. Like, we could submit any time and then update whenever they submit the final one, sort of thing. But let's say we have, like, a date started type of thing. Maybe we wanna, like, store that whenever they start this form submission. So we could track things like how long it actually took them to do this. For that we want the date time. We're just gonna do time stamp. Cool. Day started. Date submitted. Let's make sure we show that field. I'm just going to make one of these full width so I can get what I want out of this. Okay. Alright, so the form submission, we've got to link that back to an actual form as well. So we will do a mini to 1 here. This will be our forms. And let's do forms as the related collection. Now if I open up advanced mode, I can also create the inverse relationship as well. So if I wanted to do something like this where inside forms I've got a relationship back to the submissions, which in this case makes a lot of sense because of probably want to see the submissions. We could set up that and create that relationship in a single go. So there's our form. And then the next thing that we want to do is create a form, I'm gonna call it form submission answers. It could just be form answers, I guess. Always like to, have like a a common prefix, among these things, especially when you start building projects that have a ton of tables in them. Alright. So there's probably a sort on that specific answer. Do we need any of these additional fields? Probably not, right? So when I think of the answer, we've got a relationship back to the form field. So we'll just call this field. Let's Create that. There's the form fields collection. And again, I could do the same kind of thing. And setting this up this way, you know, with all the different database tables and the collections inside Directus allows us to, you know, say I wanted to query all the fields or all the answers from a single field on the form. This makes that a lot easier as well. So if we go in here, let's just add that corresponding one to many relationship as well. Let's call this answers. Great. And then we've got, what, just like a value? I'm I'm just gonna simplify this. I'm gonna sort everything as text. You know, if you had different field types or like numbers or dates, maybe you wanna store those in the underlying database as that specific data type. You know, you could set up some different fields for that like value date, value text, value number, etcetera. But to keep this easy we're just gonna store everything as a value. Alright, so now if we just poke around, right, if I were to like manually add a submission here, I could click create. I'm not seeing the actual answers relationship here. Form answers, form submissions. Oh, that that would be why I'm not seeing it because we haven't created it yet. So the last thing that we need to do is link this form answers to the actual submissions. So we'll go in and on the form submission, this is going to be a one to many relationship, so we'll call this answers. And the submission ID, great. We'll show a link to that item. Just inspect this. It looks great. We got a sort field for those answers. Cool. Everything should be gravy. Right? So what's helpful sometimes, especially, like, if you're doing CRUD operations from the front end, maybe I wanna go into this form and I add a new submission manually. Alright. So just say this. Great. Add some answers. Here's our field that we're answering. Bryant. Create a new one. Last name, Gillespie. And this is a little bit cumbersome, but, again, we're gonna be handling this from the front end. Gmail.com. How can we help? This is gonna be what? Sales rep. And then some comments. Comments. Okay. Alright. So now we've got a submission. I can see kind of the answers here, and these are not super helpful again because we haven't adjusted like the display templates for those. So our form answers, maybe we want to show the the field name, then the actual answer. Great. Let's set that up. For our form submissions, maybe we want to show the, like, the date this was created, submitted. We wanna show the form as well. So we'll just show form dot title. And let's just edit this. Underneath the hood, this is just much syntax. We can also do dot notation to access the related fields as well. Set a little bullet point. All right, so now if we go into forms we can see the contact us form submission a minute ago. And now I can see the individual answers here, which is kind of helpful, right? But let's take a look at the structure of this on the other end. So if I wanted to see an individual submission, I'm just gonna pull this up, copy this, we've got items, form submissions. And then I can get the actual submission here. So here's kind of the structure of this, and this is what we would need to post as well, right? So we've got a form ID, then we have the individual answers. If I do question mark fields, we do just all the root level fields, we grab the answer fields, see what what kind of thing that we need to pass, right? So we got the field ID that we're gonna need to send via the API request when we submit that. And then we've got the value, so what the value of that is. Okay. So now let's take a look at actually submitting this form inside here. So let's create a new function. Let's call it Async function. We'll just call it Submit Form. Great. And what are we gonna do here? We're gonna have, like, a transform the form data to the format you need, submit to the Directus API. Alright. So if we look at our actual form data, how do we get this set up? So it's just an array of answers. I probably could have made this even simpler than that, honestly, and could have done, what, just the the form field ID. Let's just test this out. Right? Form data dot field. Let's do it this way. Answers. Form data. We still do answers. Just make that an object, and then we can add the ID for that. Async function. Oh, misspelled function. Just wondering why that wouldn't compile. Colosbybryant@gmail. Great. And now if I just refresh down here, click form, do we see our form data? There we go. We've got that. Cool. Alright. So when we submit this form, let's, transform this data. So, we're going to map through the answers. So we will do data. This is a good ID as well. We're gonna need the form ID. So this will be route dot params.form. Then we have the answers, but we're gonna need to map those first as well. Map answers. Alright. So object what do we want? Object dot values, object dot keys, form data dot answers. Yeah. There we go. For each. So we're gonna do the data key. Let's just take a look at our structure here. We've got the answers. So we need an ID. No. We don't need an ID for the actual answer. Directus should create that for us, but we do need the field ID. Alright. So we've got answers here. This is gonna be an actual array. And then for that, we're going to do answers dot push field dot key dot value. Answer is key. Great. Hey. We do console dot log just to test this out. Let's Data to be submitted. Save this. See what we got, Bryant. Oh, got all this selected. Gillespie. Okay. And then let's add submit or at click submit form. We refresh really quickly. Right. Gillespie. And now if I hit submit, Form data. Oh, that's a value. That's reactive. So let's do, like, an on that. Bryant Gillespie. Hit submit. Cannot read properties of undefined. Field uncaught in promise. Object. Keys. Answers.key. Form data answers. Console dot log, form data. Let's just unref this form data to see what we're getting. Brandt. Form data. Target. Answers for each key, data dot answers dot push, field key, form data. Oh, duh. Gotta unref that again. Silly sometimes. Great. So now I can see here's the data to be submitted. There's the field. There's my value, etcetera. Now all we should need to do is actually send this. So, let's do create item from the Directus SDK. And just the same way we can fetch those, relationships, those related fields in a single call, we can also push responses to the related fields in a single call as well. So we'll do let's do data. Let's just see it this way. I can't remember what's gonna be coming back. The async data call returns reactive values for data. But we're not gonna use async data here. This will be just like regular fetch. So we'll do await. Actually we can just use the directus SDK here as well. So we'll do directus dot request, create item, and this will be form_ submissions data. Is there anything specific we want to return from this? Nope. Let's wrap this in a try catch and just console error any errors that we receive. Form underscore submissions with the data. Console dot log response. Great. Let's see what we got. Alright. So we're just gonna pick on teammates here. We'll do a Matt Minor. Maddie atexample.com. I need a developer. Help me, please. Alright. So we hit submit. Do we see an actual network call going out here? Post forbidden. Oh, no. What are we doing wrong? Right? Where are we at on time? We got 10 minutes left. We're we're in pretty decent shape. What we don't have here is the permissions to actually create items in form answers and submissions. So we go into our public option here. We can see that, we've got the ability to read form fields and read forms, but we don't have the ability to create form submissions and create answers. Alright? So there's a couple different ways that you might wanna set this up. You know, if you wanted to, like, potentially route submissions through, directly to the Directus API or, you know, through some other, like, server side redirect, inside Nuxt through like a server route or something. Or in this case, we can just give direct public permissions to create these things. Now one of the other things that you might wanna do on the read side of it, you know, I I might want to use like a custom permission or something like that so that, people can't read all the actual form submissions. So maybe just the, IDs, etcetera. Or I could even leave this off as well. I'm just gonna enable this just while in testing. Just remember to restrict this before you go to production. So now if we test this form submission again, what do I get? I get a 200 okay. There's our submission. If I go into Directus, we can see we've got our form submission there. There's all of our answers. That's great. We don't have the date started because I didn't populate that. Right? But what if we were just to do something like this where we've got the date underscore started, And we do new date. Great. And this, let's say to ISO string. Just to be sure Directus will swallow that down. Alright. Let's test this one more time and maybe we can fix the field IDX, form field dot link, currentfieldidx. That's where we goofed up here. Okay. So now we've got Bryant. We got Gillespie. We'll just add a bunch of junk here. Email test at example.com. I need a developer. Oh, I like 35 submit buttons here. Let's go ahead and submit that down. Submit that up and see if we've got that date started field. Why do I not have that? Didn't populate. Oh, I didn't actually pass that in there. Date started. Current field, IDX. This actually needs to be just out of here, doesn't it? Alright. Bryant Gillespie test@example.com. Got a developer. Here's some comments. There's our submit. Great. Take a look at our submissions. There we go. We've got our date started. So now we could actually, like, run calculations on how long it took to fill out this form. We've got all of our answers in the correct place. We've got, like, 6 minutes left. Did we check all the boxes here? Right? We've got a form. We've got the fields. We built the form here on the back end. We've rendered and submitted the form on the front end. Amazing. Let's see if we could just take this a step further. Maybe we wanna create a dashboard for all of our form submissions. Let's see what we can get here. Maybe we do a simple metric. We want to just track the number of form submissions. So that'll be the field ID. We'll just do like a count of that ID. Total form submissions. Boom, I can see we've got 4, right? One of the other things that I like to do when building these dashboards inside Directus is make this dynamic. So what I can do is, let's say I wanted to see the answers for a specific form. What I could do is create a global relational variable. We'll call this, like, form ID. So we'll pick the form. Great. Show that out. Select a form. See what we got here. I'm just gonna move this out of the way. So we can select a form, that variable is named form ID, and then maybe I want to show a list of responses. So let's show the form submissions. And for the filter here, what I want to do, let's just show the date that that was submitted. Great. Under the filter section, what I can do here is go in and I'm gonna create a filter that uses that, that variable that we set up. So instead of hard coding a form value here, I could just do something like this where I say form underscore ID equals that variable and just wrap that in the mustache syntax. So responses for this form. Alright, so when I save this, we shouldn't see any responses here. But if I pick our contact us form, we can see those, then I can open each of those up. Or, you know, we could also change this to something like field ID. So if I wanted to see responses for a certain field, so we'll just change this from forms to a form field. We can go in, the display template for this, maybe we want to show the name of the field. Maybe we want to show the name of the form that it comes from, the title of that form, where are you. There we go. We don't need a filter there. That's gonna be our field ID. How are we doing on time? We still got 2 minutes for this. We'll change this to select a field. Field. Great. Responses for selected field. And then we'll just do the same thing here. We're just gonna change this, right? We'll change the collection to form answers. For our displayed template in this actual list, maybe we want to see the value. So we'll just show the value. And instead of this, we're gonna do the field dot ID equals field underscore ID wrapped in mustache syntax here. Alright, so now if I select First Name I can see a list of all the responses. Great. Looking great. And then maybe we even duplicate this. And let's say I wanna show those values in like a chart or something, right? So we got our donut chart here. We got our styles. The field that we want to count in that case is gonna be the value. And now I can get a breakdown of all of those responses. Great. Cool. 1 minute 24 seconds left. Ring the bell. Again, this has been a fun episode. You know, taking this from here, obviously, like, I would totally improve the styling, add some validation into it, but this would be super handy to have a form builder that you've got direct control over, that you don't have to pay outrageous prices for, well, like 30 submissions or something like that. I don't know. Let's look at the pricing. What do we have on Typeform? I like Typeform a lot. The pricing is a bit crazy. Alright. $25 a month gets you or $29 a month gets you a 100 responses. Using Directus, get way more than that for less. Amazing. Alright. That's it for this episode of 100 apps, 100 hours. I hope you've enjoyed it. I sure have. We'll catch you next time.","e02b678a-4832-4658-8fac-490a73dce2fd",[546],"fad4d940-21cd-4966-9a11-e5951a79c61d",[],{"id":157,"number":158,"show":122,"year":159,"episodes":549},[161,162,163,164,165,166,167,168,169,170],{"id":146,"slug":551,"vimeo_id":552,"description":553,"tile":554,"length":555,"resources":8,"people":8,"episode_number":131,"published":556,"title":557,"video_transcript_html":558,"video_transcript_text":559,"content":8,"seo":560,"status":130,"episode_people":561,"recommendations":563,"season":564},"ai-letters-to-santa","1059428648","Bryant builds a holiday-themed app that generates personalized letters from \"Open Source Santa\" based on GitHub profiles. Watch as he creates a system that analyzes developers' repositories, determines whether they're on the open source naughty or nice list, and generates snarky, sarcastic letters from Santa — complete with festive styling and holiday cheer.","6209314e-e6ee-4a2d-9e97-11eedd08595a",59,"2025-03-10","Mission: AI Letters to Santa","\u003Cp>Speaker 0: Alright. Alright. Alright. We are back with the Christmas edition of 100 apps, one hundred hours. Today, we are going to be building AI letters from Santa.\u003C/p>\u003Cp>I've got my lumberjack style on today. My wife called this lumberjack Jesus earlier, but I digress. We're back for more. The rules of 100 apps, one hundred hours. If this is your first show, we have sixty minutes to plan and build an application, a website, a portal, whatever.\u003C/p>\u003Cp>Whatever we're building, sixty minutes, no more, no less. And rule number two, the anti rule, use whatever you have at your disposal. And since this is an AI Christmas special, I'm gonna pull out all the stops. So let's dive right in. We're gonna hit the clock here.\u003C/p>\u003Cp>Fire it up. Sixty minutes on the timer. Go. Alright. So AI letters from Santa.\u003C/p>\u003Cp>What do we actually want out of this? So I have to admit, I cheated a little bit because I thought about this with my team, and I knew we wanted to do this. I've seen things in the past where you write a letter to Santa, you get something back in the mail, etcetera, etcetera. With AI, we could take this up a notch. So combining two ideas.\u003C/p>\u003Cp>A while back, I saw a GitHub roast page where you enter in your GitHub profile and it, basically will scrape that and give you a roast of how well you're actually not doing in GitHub. So we're gonna combine that with a letter from Santa. And basically, what we wanna do is, enter a GitHub profile. We're gonna scrape that profile. We're going to send that to AI.\u003C/p>\u003Cp>So we want LLM analysis of the profile. I'm not sure what we're gonna call that. And then I'm going to bucket people on the open source naughty or nice list. So score naughty or nice list. And then we're gonna generate a letter from open source Santa.\u003C/p>\u003Cp>Generate a letter from open source Santa to that GitHub profile to that profile. Alright. So as far as that functionality, this looks pretty good. Right? What are the tools that we're gonna use of the trade today?\u003C/p>\u003Cp>I've got a Directus Docker container up and running locally. Directus is obviously the back end we're using to store all of these things. And if everything works as intended p m p m g. I guess, sometimes things don't work as you intend. I've got a Nuxt application that we are going to try and use here.\u003C/p>\u003Cp>I'm not sure what's going on, but let's hop into the Directus instance. So I'm just gonna pull up Chrome. We'll log in to 8055, and I should be able to pull up my back end. So great. Got Directus running.\u003C/p>\u003Cp>You could see this is a pretty blank instance of Directus. This is just the boilerplate I use now. There are a couple extensions installed that I was testing, just messing around with. But, let's make sure. What are we doing here?\u003C/p>\u003Cp>For MPMI. Sometimes these things never go as planned. Okay. So maybe now we can get this Nuxt application up and running that will be served at local host 3,000, and we'll just basically use it to scaffold out our communications. As far as what I'm using, I've upgraded this boilerplate that I've used for 100 apps to, the Nuxt UI v three alpha, just to play around with Tailwind four and, you know, some of these nice new components that are coming from, like, Radix view.\u003C/p>\u003Cp>So alright. Let's actually model this thing out. Right? What do we need as far as our data models? I think we just need, like, a maybe like a profiles.\u003C/p>\u003Cp>So under profiles, we would have, what, our username, letter from Santa, letter from Santa, list, you know, are you naughty or nice? Great. And what else do we need? Let's let's jam on that. So we'll, just set up the back end for this.\u003C/p>\u003Cp>I'm just gonna create a new collection. We're going to call this profiles is the name of it. And why can't I zoom way in? There we go. That's maybe too far, but all good.\u003C/p>\u003Cp>Let's do created at, updated at. K. Status sort not needed. Who this was created by, I'm not super concerned with. So now we have a profile.\u003C/p>\u003Cp>We're gonna do the username. Great. That's where we'll store the GitHub profile. What else do we need? What else did we have here?\u003C/p>\u003Cp>We've got the letter from Santa. What is that gonna be in letter from let's just call it letter. Great. We'll use the WYSIWYG editor inside Directus so we could just store, I'm assuming, HTML content for that. And then we've got the list.\u003C/p>\u003Cp>So that's basically gonna be a string. We can, you know, make this look nice inside the directus admin. We'll just give it a naughty. Feel naughty just typing that out, and then we have the nice list. Great.\u003C/p>\u003Cp>There we go. And I'm just going to go on record that we're probably as soon as we start typing naughty into the AI stuff, we'll probably get some some things back. Like a I probably set off the content alarms or something like that. So there we go. We've got a username.\u003C/p>\u003Cp>We've got a letter. We've got a list. You know, I could potentially put that in here. What I'm gonna do now, I'm just gonna let's just we're just using Directus to store this at the moment. Right?\u003C/p>\u003Cp>There's a lot of different ways I could go with how to actually generate the application here. But Directus allows me to create custom extensions. What I'm gonna do here is just start, working on this from the Nuxt side of it. So we're gonna input the, actual form here. Let's add a profile.\u003C/p>\u003Cp>What are we gonna call this? Let's just call this letters dot view. We'll get a view component set up. Lang equals TS. Great.\u003C/p>\u003Cp>I need to work on my little macros here. Okay. Alright. The other thing that you'll notice here is that I am using cursor. So cursor I recently started testing this thing out.\u003C/p>\u003Cp>Really enjoying the actual auto completions for this thing. So, I don't have it usually generate like a a giant list of code, but the automations, or the auto completions are are pretty nice for this thing. So let's start with, what, step one. It'd be enter GitHub profile info. GitHub username.\u003C/p>\u003Cp>Alright. So the only thing here, sometimes it gets a little wonky with the okay. So we use the you form from Nuxt UI. Good question, Brian. The new one, the alpha, they changed some of the conventions.\u003C/p>\u003Cp>So I've got a form with a schema. I've got a form field instead of a form group, and then I've got an input. Okay. So we've got the form, new form field, and input GitHub username. Let's just see what that gets us on the front end.\u003C/p>\u003Cp>We're gonna go to this page, which is letters. Okay. Alright. So let's go ahead and just center this up. I think there's actually a container component we can use.\u003C/p>\u003Cp>Great. Cool. Okay. So now we have a GitHub username, and let's add a submit button. New button, click handle submit, and boom.\u003C/p>\u003Cp>We have a GitHub username, blah blah blah. Hit submit. Supposedly does something. What it's gonna do right now? Absolutely nothing.\u003C/p>\u003Cp>Alright. So the next thing that we wanna do, let's kick this thing off. We want to have the form state, we use reactive for that. Great. GitHub username.\u003C/p>\u003Cp>Okay. And then we're gonna write a function to handle submit. Thank you. Yeah. Great.\u003C/p>\u003Cp>We'll just, console log that. Right? Boom. There we go. We can see the GitHub username, yada yada yada.\u003C/p>\u003Cp>Alright. This is actually gonna be an async function. Great. Okay. So now what do we wanna do with this?\u003C/p>\u003Cp>Right? We have to think about our application structure. And what I'm gonna do here is just basically add a Nuxt server route. So if we break this down, in this server route, what we're gonna do, call the GitHub API, call GitHub API. We're gonna wanna grab a couple pieces of information like the user profile, or any other repos, and maybe, like, their their public read me.\u003C/p>\u003Cp>I guess we could loop through the actual repos and, you know, pick up more information there, but, let's see what we can get done with that piece. Alright. So let's just go here. We're gonna set up a new route. Let's call it, roast route.\u003C/p>\u003Cp>We'll do post, and just gonna copy the event handler here. K. So now whenever we hit this route with a post, it should return hello world. We can just check and see if that's actually gonna work. So, we will do the I'm trying to think if that's gonna return.\u003C/p>\u003Cp>Nope. So we got the response. We're gonna do await. We can use the regular fetch or the dollar sign fetch, which is the Nuxt specific version. And just test this out, see what we get back in the console.\u003C/p>\u003Cp>Where are you? Okay. Yeah. So we can see the request going out. We can see hello world coming back.\u003C/p>\u003Cp>Great. Cool. Alright. So now what we're gonna do, right, let's just scaffold this out. We are going to pick up the body.\u003C/p>\u003Cp>There's a wait read body. Great. So that is going to have the GitHub username in there. And, how did we spell that? Yep.\u003C/p>\u003Cp>Great. Alright. So we're gonna say GitHub username, and then we've got, like, this git roast function. I'm not really sure where some of these auto completions are coming from. But, what we're gonna do next, let's call yep.\u003C/p>\u003Cp>There we go. That's a good one. API users, GitHub username. Is that the correct one? Let's just test that.\u003C/p>\u003Cp>All the developers on my team are screaming and crying at the moment, watching all these AI auto completions. So that seems fair. Great. And let's actually use the Nuxt equivalent. Just this is using OFETCH, which does some automated data transformation and should automatically throw errors for you as well, which is nice.\u003C/p>\u003Cp>So this is gonna be the let's call this a profile. Alright. And then if we take a look at the profile, we probably wanna get the actual repos for that user as well. Alright. So we'll get the repos.\u003C/p>\u003Cp>Great. And let's just take a look at the data we're getting from the actual repos. Okay. So what do we actually concern ourself with here? Do we actually want all of this information?\u003C/p>\u003Cp>What do we actually care about from these? So stargazers, watchers counts, maybe those properties. You know what? Let's just jam it all in there and see what comes out of it. Right?\u003C/p>\u003Cp>And then let's get the profile readme. GitHub user content, GitHub username, repos dot name, Repos dot name. No. That's not gonna cut it. I think it's gonna be, what, GitHub username.\u003C/p>\u003Cp>GitHub username. Somebody who's already done this before. Give me the structure. And then main. Let's just see if we can find that.\u003C/p>\u003Cp>Read me will just populate my name. I don't even know if I have a actually have a read me. Yeah. There we go. Okay.\u003C/p>\u003Cp>So that is the structure. Great. That is what we needed to confirm. And now let's just actually return this and see what we'll get back. Alright.\u003C/p>\u003Cp>Brian Gillespie. Now I'm gonna fire this away. Roast. No. Nothing found.\u003C/p>\u003Cp>Well, that's a little concerning. GitHub username equals body. Read the body. We have fetched the user's GitHub username. Let's just console log the username.\u003C/p>\u003Cp>API dot GitHub users. I don't see the actual username coming back. GitHub. That's always fun. Alright.\u003C/p>\u003Cp>What did I do wrong? GitHub underscore username. Okay. Oh, duh. Are we actually passing that in the body of the form?\u003C/p>\u003Cp>Form. Console dot log response. Request payload. API slash roast dot post. What are we getting back here?\u003C/p>\u003Cp>GitHub username form dot GitHub username. Oh, that's right. We are missing a state variable here. So is it actually submitting the form? No?\u003C/p>\u003Cp>No. Okay, friends. What do we do from here? We have handle submit. We're going to use fetch await.\u003C/p>\u003Cp>Wait that fetch request. We should have already got this back. Of course. There it is. What a dumb dumb.\u003C/p>\u003Cp>Forgot to actually fix the v model there. So that's what that is. Sometimes, these are not great to do at the end of the day. But okay. Where we at as far as time?\u003C/p>\u003Cp>We've got forty two minutes remaining. I feel pretty confident on this one. Alright. Now with our roast, we can remove this. We should be able to get that information.\u003C/p>\u003Cp>Now let's make sure that we're getting what we want back from that API. Great. There's the profile. There's the repos, and there's the profile. Read me.\u003C/p>\u003Cp>Great. Alright. So what are we gonna do with these now? Right? The next step in this process would be to, pass the profile to LLM and ask it to summarize for us.\u003C/p>\u003Cp>What do we want this to return? It should return something that looks like this. We want a letter from Santa. Letter from Santa in HTML. And then we're gonna want the the list, naughty or nice.\u003C/p>\u003Cp>And that should be all we really need to return. Alright. LLM returns JSON. Cool. So what is the LLM we're gonna use?\u003C/p>\u003Cp>You know, typically, I use OpenAI for a lot of the stuff that I do here at Directus. I've been messing around a lot with, Claude locally. So we're just going to try this out. Santa letters. So we're gonna use anthropic.\u003C/p>\u003Cp>There's my API key. We're going to drop that in our ENV file, if I can actually get there. Jeez. There we go. I'm just gonna call it Claude ABI key.\u003C/p>\u003Cp>Okay. Great. And by the time you've watched this, hopefully, I've disabled that key. So, don't stop the video and try to figure that out. Alright.\u003C/p>\u003Cp>Let's pull up our docs for the API. We need to get the API reference. And let's define this prompt. Prompt. You are a letter writing AI.\u003C/p>\u003Cp>Alright. Analyze the following GitHub profile. You are the open source Santa Claus. You determine whose open source contributions are naughty or nice, analyze the following GitHub profile, Return a JSON object with the following fields, a letter from Santa and HTML. Set a really high bar for the nice list.\u003C/p>\u003Cp>What else do we need as far as a prompt? And, yeah, here is the data, profile, readme, json, stringify. Wonder why it's doing that. But okay. Nevertheless, there we go.\u003C/p>\u003Cp>Turn a JSON object instead of really write the letter in a snarky sarcastic tone. Cool. Alright. And now we're going to send that to Anthropic. Alright.\u003C/p>\u003Cp>So if we look at their oh, looks like we could just use their JavaScript SDK. That's great. Let's go ahead and open this up. We'll fire that up, install this thing. Import anthropic.\u003C/p>\u003Cp>Great. And then we're going to create that message. Alright. Constant AI response equals anthropic messages dot create Claude Sonnet. Okay.\u003C/p>\u003Cp>Messages user role, content prompt. Do we wanna set, like, max tokens? What is the what's the default for max tokens? Where do we actually pass this API key? Getting started authentication, x API key.\u003C/p>\u003Cp>API key equals process e n v. And, again, like, you could start to see why I really like using cursor because it has, like, this sixth sense for a lot of this stuff that I'm actually trying to do. Sometimes it gets that wrong, but a lot of times it gets it right. So alright, AI response messages. Do we wanna set a max tokens?\u003C/p>\u003Cp>Body messages, max tokens required. Let's give some more parameters. Write a short letter in a short in a snarky sarcastic tone. That is 500 words or less. And then for the tokens, if we look at Sonnet, we've got like a context window of like 200,000, so maybe a hundred thousand tokens.\u003C/p>\u003Cp>Oh, no. That's the output. Max output is eight nine one two. That's fine. Max tokens.\u003C/p>\u003Cp>Great. And let's return. Actually, what we're gonna do next is save that to the Directus database. Right? So we've got this collection for our profile.\u003C/p>\u003Cp>What I've also done, I've got a utility set up here. This is just using the Directus SDK. And one of the nice things about Nuxt, I say that a lot, is, the ability to it will auto import this for me. So I don't have to import it. I should just be able to call Directus server right here.\u003C/p>\u003Cp>So let's call it Directus response equals await directus server dot request create item. That's going to be in the profile. And we'll do the GitHub username. That's actually going to be username. Letter response, content dot text.\u003C/p>\u003Cp>I don't actually know what we're gonna get back directly. Return only a JSON object. And maybe we wanna add something like this for let's just do code. We'll set this up. And I'm just gonna add a field for, let's call it metadata or something where I'm just gonna store the entire response.\u003C/p>\u003Cp>And honestly, let's just do that to begin with. Metadata, AI response, content dot text. So if we take a look at the API reference, we go back to messages here. I'm kinda curious as to what we're gonna get back. The content text.\u003C/p>\u003Cp>Okay. Type text something. We'll get back something from the system. Let's just even do it this way. We'll say content direct us response, and then we're going to return direct us response.\u003C/p>\u003Cp>See what that gives us. Now let's go in. Where's our app? We'll switch back to Chrome. I do like Arc.\u003C/p>\u003Cp>I've found it to be lacking for development because it's just not super fast. Alright. So fingers crossed that this actually does what it should do. And, let's make this even nicer. And we'll add, like, a loading state, constant loading, ref equals false.\u003C/p>\u003Cp>We'll add loading dot value equals true. Loading dot value equals false. Great. And what else do we want to do? Is there a loading state on the actual form?\u003C/p>\u003Cp>Let's take a look. So Nuxt UI state, there is not a loading state on that. There should be on the button though. So just update that. Okay.\u003C/p>\u003Cp>And let's test this bad way out. Submit. Alright. We're waiting. We're waiting.\u003C/p>\u003Cp>We're waiting. We're waiting. We're waiting. This could take a minute. So, you know, we might even want to, like, potentially set up a oh, okay.\u003C/p>\u003Cp>So we're not getting anything back. We see a request error. So let's go into our roast, and we should probably do some error handling. Alright. Catch error, console error.\u003C/p>\u003Cp>Return, or we could just throw the error. What do we got here? Format. Alright. Let's refresh.\u003C/p>\u003Cp>I'll try this again and see what kind of error we're getting and why. Pending. Invalid user credentials for Directus. Okay. Great.\u003C/p>\u003Cp>So, just wasting tokens there, throwing them into the void. One of the things that you'll notice, I do have this direct as URL set up, but, my server token is probably a % not correct. So I'm gonna go in and create a token for this. We'll just create a new token. We'll call this the server token.\u003C/p>\u003Cp>And I wanna make sure in my utility that I have that set as server token, direct us URL. Okay. Let's try this thing again. PPM dev. I will restart the dev server, pull in that new ENV, though I think Nuxt may automatically update that for us.\u003C/p>\u003Cp>How we doing on time? We got twenty nine minutes left, so I'm feeling pretty confident that we can get something out of this. Let's go ahead and try it again. Bryant Gillespie. Submit.\u003C/p>\u003Cp>K. K. Roast. You do not have permission to access this. Okay.\u003C/p>\u003Cp>Can anybody spot the error? It is because I left off a s. We have profiles, and this is profile. So again, if I I don't know if I you can actually see the logs for anthropic. Okay.\u003C/p>\u003Cp>Yeah. We could see here's the actual logs. It's probably not showing what we've got there. But anyway alright. We'll try this one more time.\u003C/p>\u003Cp>Let's just clean this up a bit. And away we go. Dun dun dun. I don't like the looks of this, actually. Let's just reset.\u003C/p>\u003Cp>Try this again. AI response. We got the prompt. Got the profile. Dun dun dun.\u003C/p>\u003Cp>The moment of truth. Are we actually gonna be able to get this thing to work? Brig Gillespie. Submit. Obviously, this would probably be better as, like, a background job or something like that.\u003C/p>\u003Cp>Alright. So we refresh, and we have something here. Okay. Yeah. So we're getting some text back.\u003C/p>\u003Cp>It looks like we need to parse the JSON. The letter is going to be text parsed response, text, parse response dot list, and then we get metadata, which would just be the parse response, I'm assuming. Alright. We're gonna delete this out. Let's run this again.\u003C/p>\u003Cp>And hopefully I'm not burning through all these credits that I loaded up. Okay. So now we're looking great. Okay. So we have our username.\u003C/p>\u003Cp>We've got our letter. Ho ho ho. What do we have here? Another developer thinking they can impress Santa with a few measly repositories. I've seen l's with more impressive profiles.\u003C/p>\u003Cp>I got made it to the naughty list. Great. Amazing. Alright. So now that's working as intended.\u003C/p>\u003Cp>Let's let's make this pretty. Right? The form, we're going to do max width. Maybe we set this to Excel. Move that form to the somewhat in the center of the page.\u003C/p>\u003Cp>And let's just lean on AI here. Right? This is already pretty cool. One of the other things I wanna do is maybe we set up a route where we actually surface this letter. Right?\u003C/p>\u003Cp>So if we do let's do letters as a directory inside pages. And we're gonna do the username in brackets. So just take this username, make that in brackets, and then I'm gonna put letters inside here, and we'll change the name of this to the index route. Alright. So let's just clean this up a bit, wrap this up, and console the error loading.\u003C/p>\u003Cp>Actually, we could do that in finally. Great. And what we're gonna do, if the response is good, we could navigate to the username page. Cool. And that way, you know, basically, like, this could get very expensive if if you made this thing public.\u003C/p>\u003Cp>Right? You don't want people generating like 35 letters to Santa. So we can add a check to the database if we've already got that GitHub username and just return the letter that we we already have. Right? Okay.\u003C/p>\u003Cp>So on the response, as long as there's no error, we're going to navigate dot to form. Github username. And this would be await. Navigate to. Cool.\u003C/p>\u003Cp>Alright. Now let's just lean on AI and see what we could do. Add some Christmas theming to this. Let's see what this actually will do. Add some Christmas thinging, ho ho ho.\u003C/p>\u003Cp>Looks like it's generating some random messages. Code review letters to Santa, random message, decorative elements. Great. Love decorative elements. Now, with cursor, I'm just gonna click apply here.\u003C/p>\u003Cp>It should go through and run through this actual code. I can close this out and see, you know, in kind of a preview way what it's gonna change. And if we hit reload oh, what we got going on here? Letters index. Is that because I changed the route?\u003C/p>\u003Cp>Okay. Yeah. Now we're looking very festive here. This looks this looks great. AI, what can you do?\u003C/p>\u003Cp>Alright. The other thing I see, maybe we want this to be block. Will that get it done? Block. Class.\u003C/p>\u003Cp>Let's just make the width full. Width full. Okay. And then let's shrink this actual form a bit. Yeah.\u003C/p>\u003Cp>MD. There we go. Alright. We're deep in the Christmas cheer now. And, while we wait, let's well, not while we wait.\u003C/p>\u003Cp>Let's actually go in and now we're gonna work on this letter. Alright. So, this does have a Nux plug in. This is just my boilerplate where I can go in and actually request the information from Directus on the client side, or, you know, I could set up a route for this on the server side in Nuxt. Both of those ways are are totally valid depending on your application.\u003C/p>\u003Cp>Obviously, totally up to you. We will just, let's let's keep it the same theme. We're going to, like, fetch roast, or we could do, like, a roast.git.ts. And what are we gonna pass? Do we want to pass the username as a param, or we'll just pass it as a query parameter?\u003C/p>\u003Cp>Okay. So in this one, what we're gonna do, we will call the profiles endpoint inside Directus. So we'll just go const, response equals await Directus server. And, you know, sometimes you wanna make requests on the server side. That's why I've got this set up, this way.\u003C/p>\u003Cp>We're gonna do read item, and I gave this a UUID. We could've used the actual profile as the primary key. But, what we're gonna do, read profiles, and we're gonna set up a query for this. So we'll do a filter parameter, the username. So that's the field.\u003C/p>\u003Cp>We're gonna drop down again. This will be equal to the username. So first we're gonna have to get the username equals get router param. Nope. We're gonna do git query, and that would just be the query.\u003C/p>\u003Cp>Great. Username. We could destructure this if we wanted to. Return username equals username, and we're gonna return that response. Great.\u003C/p>\u003Cp>Cool. So now we do this. And on this one, what we can do is use the use fetch composable from Nuxt. So this will be we've got some data. We're gonna use fetch, and we're gonna call API slash roast.\u003C/p>\u003Cp>And the is it params? I believe. See what we got. And let's add the so the same festivities, I guess. Festiveness.\u003C/p>\u003Cp>Perfect. Alright. Decorative elements, blah blah blah, random messages. We're gonna put that up here in the script. Okay.\u003C/p>\u003Cp>Code review letters to Santa. And instead of the form, right, we're gonna replace this with data. Alright. So now if I do this, what's gonna happen? Route is not defined.\u003C/p>\u003Cp>Okay. So we just need to call use route to fetch that route. And do we actually get the stuff that we need here? We could test this API as well. Letters API roast username equals Brian Gillespie.\u003C/p>\u003Cp>Okay. Yeah. So that's getting us what we want from direct us except is it query? What is the use fetch? This is where, like, Nuxt documentation comes in handy.\u003C/p>\u003Cp>Use fetch. Where we at? We got sixteen minutes remaining. We got data use fetch. What are the URL query?\u003C/p>\u003Cp>Okay. Alias for query. That's what I thought. Root params username. Oh, data.\u003C/p>\u003Cp>Are we actually let's jump into the view dev tools. We'll hit the username route. And I see the data here. Here's the issue. Right?\u003C/p>\u003Cp>It is returning an array. So inside our routes, we could, you know, do something like this where we're just picking off the first item. I could also do that transform that on the the Nuxt side if I wanted to. Here's our letter from Santa. Cool.\u003C/p>\u003Cp>Code review letters from Santa. What I'm gonna do, let's use the pros class from Tailwind to get styling for this. We'll make the text dark green. Great. That's fine.\u003C/p>\u003Cp>And then the interior of this, we're just going to use v HTML. So we get this. Do I not have Tailwind typography into this? At plug in Tailwind typography. Okay.\u003C/p>\u003Cp>Yeah. So there we go. Now we've got the letter from Santa Claus. This is looking really nice. Perfect.\u003C/p>\u003Cp>Let's add like a cursive font. Right? Font family cursive. And this is Tailwind four, where all the config is basically CSS variables. So, really enjoying that Nuxt module, playing around with it.\u003C/p>\u003Cp>Let's find a handwritten font. Okay. Caveat. Looks nice. Nuxt has also added a a like this font amazing thing where you just throw your fonts in the CSS and it will actually download these things for you.\u003C/p>\u003Cp>So let's take a look at this. Right? I'm just gonna change this to font cursive and bada bing bada boom, we get what we want. So let's put, like, pros XL to XL. And there we go.\u003C/p>\u003Cp>So dear Bryant, what do we have here? Blah blah blah, etcetera. We have got thirteen minutes left on the clock. What can we do for fun? Let's go back and actually test this thing out.\u003C/p>\u003Cp>I'm just gonna refresh. There we go. I'm gonna do our fearless leader here at Directus, mister Ben Haines. We're gonna send this to Santa, and something bad happened. We could not find okay.\u003C/p>\u003Cp>So it looks like this one is not finding Ben's profile. Haynes, Maine. And Haynes Haynes Haynes Haynes Haynes. Would that be at, like, Master branch maybe? Where's our roast?\u003C/p>\u003Cp>Roast.post, profile read me. Try. I bet it's at master. I'm just gonna do this the quick and dirty way. Alright.\u003C/p>\u003Cp>So we go back. Let's try this again. Mister Ben Haines, we're about to roast you, sir. Alright. So we're checking the list twice.\u003C/p>\u003Cp>And eleven minutes on the clock. We've got the letter to Ben Haines from Ben Haines. Why are we not seeing the actual letter? There it is. I'm dreaming of a Nuxt application that actually works.\u003C/p>\u003Cp>What is going on with this? Letters, username, data dot name. I'm assuming because there is no name. Username. I'm running this on a sour note here.\u003C/p>\u003Cp>Ben Haines. That's kinda weird. Ben Haines. Why is it doing that? API roast username.\u003C/p>\u003Cp>What is going on here? Get async data. API roast. Why does it work for me and not for mister Haynes? What are we actually doing wrong here?\u003C/p>\u003Cp>Did I spell the name wrong? GitHub username. Alright. E pipe. Use fetch roast.\u003C/p>\u003Cp>Can't find the username? Profiles get username, get query. Is it read query? No. It's get query.\u003C/p>\u003Cp>Return query. API API roast. Ben Haines. So why aren't we why isn't this working? So it's not actually finding the username for that, which is odd because I have the username right there.\u003C/p>\u003Cp>Filter contains. Okay. I don't understand it, but we're gonna roll with it. Great. Some type of encoding or something maybe.\u003C/p>\u003Cp>Not sure. Booyah. Ben, I'm gonna read this to you. Dear Ben, ho ho ho. Well, isn't this embarrassing?\u003C/p>\u003Cp>I've been reviewing your GitHub profile, and I must say I'm thoroughly underwhelmed. 20 whole repositories, you must have been super busy this century. Meanwhile, Santa's got billions of believers worldwide. Look, I'm not saying you're on the naughty list because your contributions are lackluster. I'm saying if you were open source for coal, you barely have enough to heat a dollhouse.\u003C/p>\u003Cp>That is brutal. So let's call that a win. This is AI letters with Santa. Do we wanna do one more just for fun? Just for giggles?\u003C/p>\u003Cp>Let's let's test this out. Directus Directus. I forget Reich's actual, GitHub profile. There it is. Okay.\u003C/p>\u003Cp>So we're gonna throw mister Reich Van Zanten in there, our CTO, see what comes out of this thing. Hopefully, we got everything we need. It will do its thing. And and, that's not gonna pick on Wrike. Yeah.\u003C/p>\u003Cp>I don't know what's going on with this thing. Potentially some kind of caching issue. Don't know. Anyway, response zero. Down to the wire, five minutes, four minutes, three minutes, two minutes, no minutes.\u003C/p>\u003Cp>Is the server running? Use async data. And what if we just use fetch? Response. It's gonna be response dot letter.\u003C/p>\u003Cp>Oh, boy. Response fetch, browse, params, username. Come on. Failed to stringify the server logs. What is going on?\u003C/p>\u003Cp>This feels like a crappy way to end this one. It should be, like, festive with holiday cheer. I don't understand what is going on with Nox. Oh, duh, dummy. You have to wait the promise.\u003C/p>\u003Cp>Is that getting us what we need? Still not getting us what we need. Data is not defined on the instance. Where else am I getting the data at? Fested messages, data.\u003C/p>\u003Cp>Oh, if response. We're not even getting a response. SSR, undefined, undefined. Hey. That's the way the cookie crumbles sometimes.\u003C/p>\u003Cp>I'm not sure what I am doing wrong with this. I'm sure it'll come to me right after I get done with this. Is it like a key? Cash. Cash, no cash.\u003C/p>\u003Cp>Cache. No cache. No cache. Well, at least Ben's works. At least mine works.\u003C/p>\u003Cp>Not entirely sure what's going on with this little API that I've written, why it is caching this. But, hey, that's AI letters to Santa. That's the way it goes. This has been a hundred apps, hundred hours. Thanks for joining me.\u003C/p>\u003Cp>We'll catch you on the next episode. See you.\u003C/p>","Alright. Alright. Alright. We are back with the Christmas edition of 100 apps, one hundred hours. Today, we are going to be building AI letters from Santa. I've got my lumberjack style on today. My wife called this lumberjack Jesus earlier, but I digress. We're back for more. The rules of 100 apps, one hundred hours. If this is your first show, we have sixty minutes to plan and build an application, a website, a portal, whatever. Whatever we're building, sixty minutes, no more, no less. And rule number two, the anti rule, use whatever you have at your disposal. And since this is an AI Christmas special, I'm gonna pull out all the stops. So let's dive right in. We're gonna hit the clock here. Fire it up. Sixty minutes on the timer. Go. Alright. So AI letters from Santa. What do we actually want out of this? So I have to admit, I cheated a little bit because I thought about this with my team, and I knew we wanted to do this. I've seen things in the past where you write a letter to Santa, you get something back in the mail, etcetera, etcetera. With AI, we could take this up a notch. So combining two ideas. A while back, I saw a GitHub roast page where you enter in your GitHub profile and it, basically will scrape that and give you a roast of how well you're actually not doing in GitHub. So we're gonna combine that with a letter from Santa. And basically, what we wanna do is, enter a GitHub profile. We're gonna scrape that profile. We're going to send that to AI. So we want LLM analysis of the profile. I'm not sure what we're gonna call that. And then I'm going to bucket people on the open source naughty or nice list. So score naughty or nice list. And then we're gonna generate a letter from open source Santa. Generate a letter from open source Santa to that GitHub profile to that profile. Alright. So as far as that functionality, this looks pretty good. Right? What are the tools that we're gonna use of the trade today? I've got a Directus Docker container up and running locally. Directus is obviously the back end we're using to store all of these things. And if everything works as intended p m p m g. I guess, sometimes things don't work as you intend. I've got a Nuxt application that we are going to try and use here. I'm not sure what's going on, but let's hop into the Directus instance. So I'm just gonna pull up Chrome. We'll log in to 8055, and I should be able to pull up my back end. So great. Got Directus running. You could see this is a pretty blank instance of Directus. This is just the boilerplate I use now. There are a couple extensions installed that I was testing, just messing around with. But, let's make sure. What are we doing here? For MPMI. Sometimes these things never go as planned. Okay. So maybe now we can get this Nuxt application up and running that will be served at local host 3,000, and we'll just basically use it to scaffold out our communications. As far as what I'm using, I've upgraded this boilerplate that I've used for 100 apps to, the Nuxt UI v three alpha, just to play around with Tailwind four and, you know, some of these nice new components that are coming from, like, Radix view. So alright. Let's actually model this thing out. Right? What do we need as far as our data models? I think we just need, like, a maybe like a profiles. So under profiles, we would have, what, our username, letter from Santa, letter from Santa, list, you know, are you naughty or nice? Great. And what else do we need? Let's let's jam on that. So we'll, just set up the back end for this. I'm just gonna create a new collection. We're going to call this profiles is the name of it. And why can't I zoom way in? There we go. That's maybe too far, but all good. Let's do created at, updated at. K. Status sort not needed. Who this was created by, I'm not super concerned with. So now we have a profile. We're gonna do the username. Great. That's where we'll store the GitHub profile. What else do we need? What else did we have here? We've got the letter from Santa. What is that gonna be in letter from let's just call it letter. Great. We'll use the WYSIWYG editor inside Directus so we could just store, I'm assuming, HTML content for that. And then we've got the list. So that's basically gonna be a string. We can, you know, make this look nice inside the directus admin. We'll just give it a naughty. Feel naughty just typing that out, and then we have the nice list. Great. There we go. And I'm just going to go on record that we're probably as soon as we start typing naughty into the AI stuff, we'll probably get some some things back. Like a I probably set off the content alarms or something like that. So there we go. We've got a username. We've got a letter. We've got a list. You know, I could potentially put that in here. What I'm gonna do now, I'm just gonna let's just we're just using Directus to store this at the moment. Right? There's a lot of different ways I could go with how to actually generate the application here. But Directus allows me to create custom extensions. What I'm gonna do here is just start, working on this from the Nuxt side of it. So we're gonna input the, actual form here. Let's add a profile. What are we gonna call this? Let's just call this letters dot view. We'll get a view component set up. Lang equals TS. Great. I need to work on my little macros here. Okay. Alright. The other thing that you'll notice here is that I am using cursor. So cursor I recently started testing this thing out. Really enjoying the actual auto completions for this thing. So, I don't have it usually generate like a a giant list of code, but the automations, or the auto completions are are pretty nice for this thing. So let's start with, what, step one. It'd be enter GitHub profile info. GitHub username. Alright. So the only thing here, sometimes it gets a little wonky with the okay. So we use the you form from Nuxt UI. Good question, Brian. The new one, the alpha, they changed some of the conventions. So I've got a form with a schema. I've got a form field instead of a form group, and then I've got an input. Okay. So we've got the form, new form field, and input GitHub username. Let's just see what that gets us on the front end. We're gonna go to this page, which is letters. Okay. Alright. So let's go ahead and just center this up. I think there's actually a container component we can use. Great. Cool. Okay. So now we have a GitHub username, and let's add a submit button. New button, click handle submit, and boom. We have a GitHub username, blah blah blah. Hit submit. Supposedly does something. What it's gonna do right now? Absolutely nothing. Alright. So the next thing that we wanna do, let's kick this thing off. We want to have the form state, we use reactive for that. Great. GitHub username. Okay. And then we're gonna write a function to handle submit. Thank you. Yeah. Great. We'll just, console log that. Right? Boom. There we go. We can see the GitHub username, yada yada yada. Alright. This is actually gonna be an async function. Great. Okay. So now what do we wanna do with this? Right? We have to think about our application structure. And what I'm gonna do here is just basically add a Nuxt server route. So if we break this down, in this server route, what we're gonna do, call the GitHub API, call GitHub API. We're gonna wanna grab a couple pieces of information like the user profile, or any other repos, and maybe, like, their their public read me. I guess we could loop through the actual repos and, you know, pick up more information there, but, let's see what we can get done with that piece. Alright. So let's just go here. We're gonna set up a new route. Let's call it, roast route. We'll do post, and just gonna copy the event handler here. K. So now whenever we hit this route with a post, it should return hello world. We can just check and see if that's actually gonna work. So, we will do the I'm trying to think if that's gonna return. Nope. So we got the response. We're gonna do await. We can use the regular fetch or the dollar sign fetch, which is the Nuxt specific version. And just test this out, see what we get back in the console. Where are you? Okay. Yeah. So we can see the request going out. We can see hello world coming back. Great. Cool. Alright. So now what we're gonna do, right, let's just scaffold this out. We are going to pick up the body. There's a wait read body. Great. So that is going to have the GitHub username in there. And, how did we spell that? Yep. Great. Alright. So we're gonna say GitHub username, and then we've got, like, this git roast function. I'm not really sure where some of these auto completions are coming from. But, what we're gonna do next, let's call yep. There we go. That's a good one. API users, GitHub username. Is that the correct one? Let's just test that. All the developers on my team are screaming and crying at the moment, watching all these AI auto completions. So that seems fair. Great. And let's actually use the Nuxt equivalent. Just this is using OFETCH, which does some automated data transformation and should automatically throw errors for you as well, which is nice. So this is gonna be the let's call this a profile. Alright. And then if we take a look at the profile, we probably wanna get the actual repos for that user as well. Alright. So we'll get the repos. Great. And let's just take a look at the data we're getting from the actual repos. Okay. So what do we actually concern ourself with here? Do we actually want all of this information? What do we actually care about from these? So stargazers, watchers counts, maybe those properties. You know what? Let's just jam it all in there and see what comes out of it. Right? And then let's get the profile readme. GitHub user content, GitHub username, repos dot name, Repos dot name. No. That's not gonna cut it. I think it's gonna be, what, GitHub username. GitHub username. Somebody who's already done this before. Give me the structure. And then main. Let's just see if we can find that. Read me will just populate my name. I don't even know if I have a actually have a read me. Yeah. There we go. Okay. So that is the structure. Great. That is what we needed to confirm. And now let's just actually return this and see what we'll get back. Alright. Brian Gillespie. Now I'm gonna fire this away. Roast. No. Nothing found. Well, that's a little concerning. GitHub username equals body. Read the body. We have fetched the user's GitHub username. Let's just console log the username. API dot GitHub users. I don't see the actual username coming back. GitHub. That's always fun. Alright. What did I do wrong? GitHub underscore username. Okay. Oh, duh. Are we actually passing that in the body of the form? Form. Console dot log response. Request payload. API slash roast dot post. What are we getting back here? GitHub username form dot GitHub username. Oh, that's right. We are missing a state variable here. So is it actually submitting the form? No? No. Okay, friends. What do we do from here? We have handle submit. We're going to use fetch await. Wait that fetch request. We should have already got this back. Of course. There it is. What a dumb dumb. Forgot to actually fix the v model there. So that's what that is. Sometimes, these are not great to do at the end of the day. But okay. Where we at as far as time? We've got forty two minutes remaining. I feel pretty confident on this one. Alright. Now with our roast, we can remove this. We should be able to get that information. Now let's make sure that we're getting what we want back from that API. Great. There's the profile. There's the repos, and there's the profile. Read me. Great. Alright. So what are we gonna do with these now? Right? The next step in this process would be to, pass the profile to LLM and ask it to summarize for us. What do we want this to return? It should return something that looks like this. We want a letter from Santa. Letter from Santa in HTML. And then we're gonna want the the list, naughty or nice. And that should be all we really need to return. Alright. LLM returns JSON. Cool. So what is the LLM we're gonna use? You know, typically, I use OpenAI for a lot of the stuff that I do here at Directus. I've been messing around a lot with, Claude locally. So we're just going to try this out. Santa letters. So we're gonna use anthropic. There's my API key. We're going to drop that in our ENV file, if I can actually get there. Jeez. There we go. I'm just gonna call it Claude ABI key. Okay. Great. And by the time you've watched this, hopefully, I've disabled that key. So, don't stop the video and try to figure that out. Alright. Let's pull up our docs for the API. We need to get the API reference. And let's define this prompt. Prompt. You are a letter writing AI. Alright. Analyze the following GitHub profile. You are the open source Santa Claus. You determine whose open source contributions are naughty or nice, analyze the following GitHub profile, Return a JSON object with the following fields, a letter from Santa and HTML. Set a really high bar for the nice list. What else do we need as far as a prompt? And, yeah, here is the data, profile, readme, json, stringify. Wonder why it's doing that. But okay. Nevertheless, there we go. Turn a JSON object instead of really write the letter in a snarky sarcastic tone. Cool. Alright. And now we're going to send that to Anthropic. Alright. So if we look at their oh, looks like we could just use their JavaScript SDK. That's great. Let's go ahead and open this up. We'll fire that up, install this thing. Import anthropic. Great. And then we're going to create that message. Alright. Constant AI response equals anthropic messages dot create Claude Sonnet. Okay. Messages user role, content prompt. Do we wanna set, like, max tokens? What is the what's the default for max tokens? Where do we actually pass this API key? Getting started authentication, x API key. API key equals process e n v. And, again, like, you could start to see why I really like using cursor because it has, like, this sixth sense for a lot of this stuff that I'm actually trying to do. Sometimes it gets that wrong, but a lot of times it gets it right. So alright, AI response messages. Do we wanna set a max tokens? Body messages, max tokens required. Let's give some more parameters. Write a short letter in a short in a snarky sarcastic tone. That is 500 words or less. And then for the tokens, if we look at Sonnet, we've got like a context window of like 200,000, so maybe a hundred thousand tokens. Oh, no. That's the output. Max output is eight nine one two. That's fine. Max tokens. Great. And let's return. Actually, what we're gonna do next is save that to the Directus database. Right? So we've got this collection for our profile. What I've also done, I've got a utility set up here. This is just using the Directus SDK. And one of the nice things about Nuxt, I say that a lot, is, the ability to it will auto import this for me. So I don't have to import it. I should just be able to call Directus server right here. So let's call it Directus response equals await directus server dot request create item. That's going to be in the profile. And we'll do the GitHub username. That's actually going to be username. Letter response, content dot text. I don't actually know what we're gonna get back directly. Return only a JSON object. And maybe we wanna add something like this for let's just do code. We'll set this up. And I'm just gonna add a field for, let's call it metadata or something where I'm just gonna store the entire response. And honestly, let's just do that to begin with. Metadata, AI response, content dot text. So if we take a look at the API reference, we go back to messages here. I'm kinda curious as to what we're gonna get back. The content text. Okay. Type text something. We'll get back something from the system. Let's just even do it this way. We'll say content direct us response, and then we're going to return direct us response. See what that gives us. Now let's go in. Where's our app? We'll switch back to Chrome. I do like Arc. I've found it to be lacking for development because it's just not super fast. Alright. So fingers crossed that this actually does what it should do. And, let's make this even nicer. And we'll add, like, a loading state, constant loading, ref equals false. We'll add loading dot value equals true. Loading dot value equals false. Great. And what else do we want to do? Is there a loading state on the actual form? Let's take a look. So Nuxt UI state, there is not a loading state on that. There should be on the button though. So just update that. Okay. And let's test this bad way out. Submit. Alright. We're waiting. We're waiting. We're waiting. We're waiting. We're waiting. This could take a minute. So, you know, we might even want to, like, potentially set up a oh, okay. So we're not getting anything back. We see a request error. So let's go into our roast, and we should probably do some error handling. Alright. Catch error, console error. Return, or we could just throw the error. What do we got here? Format. Alright. Let's refresh. I'll try this again and see what kind of error we're getting and why. Pending. Invalid user credentials for Directus. Okay. Great. So, just wasting tokens there, throwing them into the void. One of the things that you'll notice, I do have this direct as URL set up, but, my server token is probably a % not correct. So I'm gonna go in and create a token for this. We'll just create a new token. We'll call this the server token. And I wanna make sure in my utility that I have that set as server token, direct us URL. Okay. Let's try this thing again. PPM dev. I will restart the dev server, pull in that new ENV, though I think Nuxt may automatically update that for us. How we doing on time? We got twenty nine minutes left, so I'm feeling pretty confident that we can get something out of this. Let's go ahead and try it again. Bryant Gillespie. Submit. K. K. Roast. You do not have permission to access this. Okay. Can anybody spot the error? It is because I left off a s. We have profiles, and this is profile. So again, if I I don't know if I you can actually see the logs for anthropic. Okay. Yeah. We could see here's the actual logs. It's probably not showing what we've got there. But anyway alright. We'll try this one more time. Let's just clean this up a bit. And away we go. Dun dun dun. I don't like the looks of this, actually. Let's just reset. Try this again. AI response. We got the prompt. Got the profile. Dun dun dun. The moment of truth. Are we actually gonna be able to get this thing to work? Brig Gillespie. Submit. Obviously, this would probably be better as, like, a background job or something like that. Alright. So we refresh, and we have something here. Okay. Yeah. So we're getting some text back. It looks like we need to parse the JSON. The letter is going to be text parsed response, text, parse response dot list, and then we get metadata, which would just be the parse response, I'm assuming. Alright. We're gonna delete this out. Let's run this again. And hopefully I'm not burning through all these credits that I loaded up. Okay. So now we're looking great. Okay. So we have our username. We've got our letter. Ho ho ho. What do we have here? Another developer thinking they can impress Santa with a few measly repositories. I've seen l's with more impressive profiles. I got made it to the naughty list. Great. Amazing. Alright. So now that's working as intended. Let's let's make this pretty. Right? The form, we're going to do max width. Maybe we set this to Excel. Move that form to the somewhat in the center of the page. And let's just lean on AI here. Right? This is already pretty cool. One of the other things I wanna do is maybe we set up a route where we actually surface this letter. Right? So if we do let's do letters as a directory inside pages. And we're gonna do the username in brackets. So just take this username, make that in brackets, and then I'm gonna put letters inside here, and we'll change the name of this to the index route. Alright. So let's just clean this up a bit, wrap this up, and console the error loading. Actually, we could do that in finally. Great. And what we're gonna do, if the response is good, we could navigate to the username page. Cool. And that way, you know, basically, like, this could get very expensive if if you made this thing public. Right? You don't want people generating like 35 letters to Santa. So we can add a check to the database if we've already got that GitHub username and just return the letter that we we already have. Right? Okay. So on the response, as long as there's no error, we're going to navigate dot to form. Github username. And this would be await. Navigate to. Cool. Alright. Now let's just lean on AI and see what we could do. Add some Christmas theming to this. Let's see what this actually will do. Add some Christmas thinging, ho ho ho. Looks like it's generating some random messages. Code review letters to Santa, random message, decorative elements. Great. Love decorative elements. Now, with cursor, I'm just gonna click apply here. It should go through and run through this actual code. I can close this out and see, you know, in kind of a preview way what it's gonna change. And if we hit reload oh, what we got going on here? Letters index. Is that because I changed the route? Okay. Yeah. Now we're looking very festive here. This looks this looks great. AI, what can you do? Alright. The other thing I see, maybe we want this to be block. Will that get it done? Block. Class. Let's just make the width full. Width full. Okay. And then let's shrink this actual form a bit. Yeah. MD. There we go. Alright. We're deep in the Christmas cheer now. And, while we wait, let's well, not while we wait. Let's actually go in and now we're gonna work on this letter. Alright. So, this does have a Nux plug in. This is just my boilerplate where I can go in and actually request the information from Directus on the client side, or, you know, I could set up a route for this on the server side in Nuxt. Both of those ways are are totally valid depending on your application. Obviously, totally up to you. We will just, let's let's keep it the same theme. We're going to, like, fetch roast, or we could do, like, a roast.git.ts. And what are we gonna pass? Do we want to pass the username as a param, or we'll just pass it as a query parameter? Okay. So in this one, what we're gonna do, we will call the profiles endpoint inside Directus. So we'll just go const, response equals await Directus server. And, you know, sometimes you wanna make requests on the server side. That's why I've got this set up, this way. We're gonna do read item, and I gave this a UUID. We could've used the actual profile as the primary key. But, what we're gonna do, read profiles, and we're gonna set up a query for this. So we'll do a filter parameter, the username. So that's the field. We're gonna drop down again. This will be equal to the username. So first we're gonna have to get the username equals get router param. Nope. We're gonna do git query, and that would just be the query. Great. Username. We could destructure this if we wanted to. Return username equals username, and we're gonna return that response. Great. Cool. So now we do this. And on this one, what we can do is use the use fetch composable from Nuxt. So this will be we've got some data. We're gonna use fetch, and we're gonna call API slash roast. And the is it params? I believe. See what we got. And let's add the so the same festivities, I guess. Festiveness. Perfect. Alright. Decorative elements, blah blah blah, random messages. We're gonna put that up here in the script. Okay. Code review letters to Santa. And instead of the form, right, we're gonna replace this with data. Alright. So now if I do this, what's gonna happen? Route is not defined. Okay. So we just need to call use route to fetch that route. And do we actually get the stuff that we need here? We could test this API as well. Letters API roast username equals Brian Gillespie. Okay. Yeah. So that's getting us what we want from direct us except is it query? What is the use fetch? This is where, like, Nuxt documentation comes in handy. Use fetch. Where we at? We got sixteen minutes remaining. We got data use fetch. What are the URL query? Okay. Alias for query. That's what I thought. Root params username. Oh, data. Are we actually let's jump into the view dev tools. We'll hit the username route. And I see the data here. Here's the issue. Right? It is returning an array. So inside our routes, we could, you know, do something like this where we're just picking off the first item. I could also do that transform that on the the Nuxt side if I wanted to. Here's our letter from Santa. Cool. Code review letters from Santa. What I'm gonna do, let's use the pros class from Tailwind to get styling for this. We'll make the text dark green. Great. That's fine. And then the interior of this, we're just going to use v HTML. So we get this. Do I not have Tailwind typography into this? At plug in Tailwind typography. Okay. Yeah. So there we go. Now we've got the letter from Santa Claus. This is looking really nice. Perfect. Let's add like a cursive font. Right? Font family cursive. And this is Tailwind four, where all the config is basically CSS variables. So, really enjoying that Nuxt module, playing around with it. Let's find a handwritten font. Okay. Caveat. Looks nice. Nuxt has also added a a like this font amazing thing where you just throw your fonts in the CSS and it will actually download these things for you. So let's take a look at this. Right? I'm just gonna change this to font cursive and bada bing bada boom, we get what we want. So let's put, like, pros XL to XL. And there we go. So dear Bryant, what do we have here? Blah blah blah, etcetera. We have got thirteen minutes left on the clock. What can we do for fun? Let's go back and actually test this thing out. I'm just gonna refresh. There we go. I'm gonna do our fearless leader here at Directus, mister Ben Haines. We're gonna send this to Santa, and something bad happened. We could not find okay. So it looks like this one is not finding Ben's profile. Haynes, Maine. And Haynes Haynes Haynes Haynes Haynes. Would that be at, like, Master branch maybe? Where's our roast? Roast.post, profile read me. Try. I bet it's at master. I'm just gonna do this the quick and dirty way. Alright. So we go back. Let's try this again. Mister Ben Haines, we're about to roast you, sir. Alright. So we're checking the list twice. And eleven minutes on the clock. We've got the letter to Ben Haines from Ben Haines. Why are we not seeing the actual letter? There it is. I'm dreaming of a Nuxt application that actually works. What is going on with this? Letters, username, data dot name. I'm assuming because there is no name. Username. I'm running this on a sour note here. Ben Haines. That's kinda weird. Ben Haines. Why is it doing that? API roast username. What is going on here? Get async data. API roast. Why does it work for me and not for mister Haynes? What are we actually doing wrong here? Did I spell the name wrong? GitHub username. Alright. E pipe. Use fetch roast. Can't find the username? Profiles get username, get query. Is it read query? No. It's get query. Return query. API API roast. Ben Haines. So why aren't we why isn't this working? So it's not actually finding the username for that, which is odd because I have the username right there. Filter contains. Okay. I don't understand it, but we're gonna roll with it. Great. Some type of encoding or something maybe. Not sure. Booyah. Ben, I'm gonna read this to you. Dear Ben, ho ho ho. Well, isn't this embarrassing? I've been reviewing your GitHub profile, and I must say I'm thoroughly underwhelmed. 20 whole repositories, you must have been super busy this century. Meanwhile, Santa's got billions of believers worldwide. Look, I'm not saying you're on the naughty list because your contributions are lackluster. I'm saying if you were open source for coal, you barely have enough to heat a dollhouse. That is brutal. So let's call that a win. This is AI letters with Santa. Do we wanna do one more just for fun? Just for giggles? Let's let's test this out. Directus Directus. I forget Reich's actual, GitHub profile. There it is. Okay. So we're gonna throw mister Reich Van Zanten in there, our CTO, see what comes out of this thing. Hopefully, we got everything we need. It will do its thing. And and, that's not gonna pick on Wrike. Yeah. I don't know what's going on with this thing. Potentially some kind of caching issue. Don't know. Anyway, response zero. Down to the wire, five minutes, four minutes, three minutes, two minutes, no minutes. Is the server running? Use async data. And what if we just use fetch? Response. It's gonna be response dot letter. Oh, boy. Response fetch, browse, params, username. Come on. Failed to stringify the server logs. What is going on? This feels like a crappy way to end this one. It should be, like, festive with holiday cheer. I don't understand what is going on with Nox. Oh, duh, dummy. You have to wait the promise. Is that getting us what we need? Still not getting us what we need. Data is not defined on the instance. Where else am I getting the data at? Fested messages, data. Oh, if response. We're not even getting a response. SSR, undefined, undefined. Hey. That's the way the cookie crumbles sometimes. I'm not sure what I am doing wrong with this. I'm sure it'll come to me right after I get done with this. Is it like a key? Cash. Cash, no cash. Cache. No cache. No cache. Well, at least Ben's works. At least mine works. Not entirely sure what's going on with this little API that I've written, why it is caching this. But, hey, that's AI letters to Santa. That's the way it goes. This has been a hundred apps, hundred hours. Thanks for joining me. We'll catch you on the next episode. See you.","ec46771b-85fb-4967-9a7e-81102f96cf74",[562],"cc9262db-1ae2-4bc1-a1f9-7d2fc51a396d",[],{"id":142,"number":143,"show":122,"year":144,"episodes":565},[146,147,148,149,150,151,152,153,154,155],{"id":147,"slug":567,"vimeo_id":568,"description":569,"tile":570,"length":402,"resources":8,"people":8,"episode_number":158,"published":556,"title":571,"video_transcript_html":572,"video_transcript_text":573,"content":8,"seo":574,"status":130,"episode_people":575,"recommendations":577,"season":578},"multisite-cms","1059434419","Amid the WordPress drama, Bryant builds a multi-site CMS system that lets you manage content for multiple websites from a single Directus instance. Watch as he sets up relationships between sites, pages, and users, then configures advanced permissions to ensure content editors can only access sites they're assigned to—all while creating a secure API structure for your frontend applications.","5e7c1362-710f-4604-a812-93290e4bdeba","Mission: Multisite CMS","\u003Cp>Speaker 0: Welcome back to the next episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. And today, we are going to be building a multi site CMS. It is been impossible to ignore the WordPress drama over the last month or two, and that has left a lot of folks wondering, hey, should I be switching our CMS from WordPress? WordPress has the multi site mode, so we're going to walk through how to build that today with Directus.\u003C/p>\u003Cp>If you are new to 100 apps one hundred hours, there are only two rules. Number one, sixty minutes to plan and build, nothing more, nothing less. Everyone knows the rules. And, the second rule, the anti rule, one that I'm definitely going to invoke today, use whatever you have at your disposal. So let's dive right in.\u003C/p>\u003Cp>We're gonna start the clock. Sixty minutes on the timer. Let's talk about multi site CMS. So what are we trying to do here? We want to manage content for multiple sites.\u003C/p>\u003Cp>That could be two, that could be three, it could be 25, it could be 50 from a single CMS instance. So we will share some of our data models amongst those sites, but the actual content will be different. So, as far as the functionality that we're looking for out of the system, let's just mock that up. Functionality. Right?\u003C/p>\u003Cp>We want to be able to manage pages and posts and other types of content for multiple sites within one CMS. We wanna make sure that there, are users can only edit and access data for sites that they are assigned to, owners of, assigned to, etcetera. That's the main functionality that we are looking for. Right? So one of the other things that I always do on these is basically map out our data model.\u003C/p>\u003Cp>Right? And if we're going after WordPress here, we have, like, pages and we typically have posts. Those are kind of the two pieces of content that, you know, when I think of WordPress, I think of, okay, here's what's inside, the data model for that. But we're also gonna need to go up to a higher level. Right?\u003C/p>\u003Cp>Because each page, each post is going to belong to a certain site. Now in this scenario, I'm thinking each site or each page only belongs to a single site and if you need to duplicate a page for another site, you just create a a entire copy of it. But, you know, you could potentially set this up if you are sharing the same page across multiple sites. You could model that relationship as well. You know, that's one of the advantages of Directus as a CMS is that you can model your data however you want, and your table structure inside your database will follow suit.\u003C/p>\u003Cp>Directus just makes those edits for you. And then, you know, we're going to need a relationship to users. Right? So users are going to be kind of a mini to mini relationship. Right?\u003C/p>\u003Cp>Love drawing these arrows. We need an arrow over here. And, let's just say mini to mini. Right? One user could be working on three, four, five sites or could be a single site, and, you know, we could have many users working on the same site.\u003C/p>\u003Cp>Alright. So as far as our data model, that's that's pretty good. You know, we'll dive into the nitty gritty details in just a moment, but I'm definitely gonna invoke rule number two here, and I'm going to go into Directus Cloud. And you can call this cheating. You can call this taking advantage of what we've already built previously.\u003C/p>\u003Cp>But we are going to do a new project using our simple CMS template because it gives us most of the functionality we need right out of the gate. So we're gonna call this multi site CMS test or 100 apps. That's good. See how long we can get the URL for this particular project. And then I'm just gonna click simple CMS here.\u003C/p>\u003Cp>I'm gonna switch this to monthly, and I'm gonna check out. And this will spin up our project for us. So, if we think more along the lines here of of what we actually need to do, You know, this simple CMS template also has forms. It has navigation, and I'll show you that in just a moment after we get it fired up. And then we have a global settings collection, inside there, which is actually, we're gonna have to do a bit of work to that.\u003C/p>\u003Cp>Can't get my arrows straight here. Jeez. Okay. There we go. Alright.\u003C/p>\u003Cp>So those all roll up to the sites. There are many users, many sites. We're just gonna wait for this Directus instance to spin up. And I'm assuming I've probably already got an email with this thing. Great.\u003C/p>\u003Cp>We'll just copy this. And feel free to steal this password here because this project should be turned off by the time we get to airing this actual episode. Alright. So we're just waiting for the CDN. Are we ready yet?\u003C/p>\u003Cp>We are ready. Okay. So great. Now we've got our login. Let's just go ahead and fire this up.\u003C/p>\u003Cp>We'll get logged into our simple CMS instance. And let's just kind of go through this actual CMS. I'll zoom way in and we can kind of dive through the content here. So we've already got some pages. We've got posts.\u003C/p>\u003Cp>We have forms, form submissions, navigation. So within pages, we can see we've got standard stuff, title, a permalink, what the status is. We've got some blocks here, of different content. So this is using the mini to any relationships inside Directus. I can drag and drop this to build a page dynamically, which is nice for our content editors or the marketing folk that are actually working on the content for the site.\u003C/p>\u003Cp>Then we have our blog posts. So lots of bunny related content here. We've got slugs. We've got status. Is this published?\u003C/p>\u003Cp>Yes or no? We got description, author, actual content. There's some AI features baked into this template. It is, a really nice starter kit for any Directus project if that's what you're looking to do. We got form fields.\u003C/p>\u003Cp>Great. There's a newsletter sign up, we need a first name, we need an email address, etcetera. And then, you know, every site needs a menu. So here we have some navigation, pretty cut and dry. And then there's the globals collection where we're storing settings that are going to be per site in our specific use case like logo, favicon, favicon, five icon.\u003C/p>\u003Cp>Great. Never know how to say that one. And we will dive in. Right? So we've got pages.\u003C/p>\u003Cp>We've got, you know, I could go in and query this particular content. So if I just drop this admin off the URL and I go to pages, we could query this content. If I wanted to see the individual content of the blocks, I can add a fields param to this and just do an asterisk, which will essentially act as a wildcard. It'll give me all the root level fields, and then I'm gonna say blocks, and I could do blocks dot collection, blocks dot item, And I can see I can start to fetch all the content within, and and I could keep drilling into this if I wanted to. But that doesn't cover our use case of multisite.\u003C/p>\u003Cp>Right? So the big glaring problem with all of this is we don't have sites. So let's go in, we're going to create our sites collection. We'll go to the settings, we'll just create sites. I'm going to use a generated UUID for this.\u003C/p>\u003Cp>We can add some of these in the canned system fields that add functionality like timestamps and users automatically. I'm just gonna leave this blank for now. And we're gonna give this site a name. And let's add a little hint for the user here. So I'm going to go to Advanced Field Creation Mode.\u003C/p>\u003Cp>I'm going to go to the Field tab and we're just going to say the internal name for this site. Great. That'll give us a hint. And voila, now we have sites. I'm gonna give this a nice looking icon, maybe a globe.\u003C/p>\u003Cp>That stands for, like, website. Right? We'll drag this up to the very tippy top. I've got globals as well. Maybe we need to change the icon.\u003C/p>\u003Cp>Let's do, like, settings or something. I used to be a designer. I like to refer to myself as a recovering designer, so, icons and user experience and small little details like that matter a lot to me. So let's go ahead and create a new site. Right?\u003C/p>\u003Cp>We're gonna do Agency OS. That's one of our other, open source projects that we released. And this is gonna be site b. Right? The other site.\u003C/p>\u003Cp>The site that I don't like. Site b. Don't even care about it enough to give it a nice name. Alright, so now what do we do with these specific sites? Right?\u003C/p>\u003Cp>Now that we have a site, maybe we want to go in and establish the relationship between all the different pages and things. Let's start with our globals though. Alright? These are settings, like the title, the URL, the logo, the favicon. Some of these we're gonna use for SEO.\u003C/p>\u003Cp>Some of them, maybe in the the footer like social links, things like that. We want to have these per site. Alright? So one of the nice things about Directus is this visual data modeling. I can come in and I can copy these fields across collections.\u003C/p>\u003Cp>So we will go to sites. We're gonna create a duplicate of that title field. I'm gonna duplicate the URL. I could even get fancy if I wanted to and potentially create, like, a flow to duplicate these things. You'll notice here on the ones that are relational, like, this is based on a many to one relationship.\u003C/p>\u003Cp>I cannot duplicate those fields just because the underlying schema changes that happen. But just regular any other fields that are not relational, I can easily duplicate. So we'll do that. We'll add our description. Copy that over.\u003C/p>\u003Cp>And let's do our social links. Now you'll also see there's this credentials group, like our open AI API key. Gosh. That's a mouthful. I wish, I wish their name was a little bit different because I'm dealing with these API keys in a lot of episodes, but I digress.\u003C/p>\u003Cp>So these things are probably gonna be they're gonna stay global. Right? We don't actually need to remove those. So here, let's just remove title. Let's remove URL now that we've copied it.\u003C/p>\u003Cp>The logo is just a nice little divider there. We'll remove the favicon. We'll delete all of these fields that are no longer global in our data model. And today's exes episode is is really just a, a big exercise in data modeling. Alright.\u003C/p>\u003Cp>So now if I go to the globals, all we have is the direct Us URL. So we're gonna serve all the the content for those sites out of a single instance. That's good. You know, they would probably share an API key or, you know, we could set it up for where that API key is per site if we wanted to. But now we can see we have these things, like, agency OS, the best operating system for your agency.\u003C/p>\u003Cp>Alright. We'll give this a URL, agencyos.dev. Run your business better. Alright. We can add social links.\u003C/p>\u003Cp>Great. Cool. Gravy. Right? And now, I could go in and, you know, I just got this option here to copy the API URL, and now I could see that individual site.\u003C/p>\u003Cp>So this is just my directus URL slash items slash site slash ID, or I can get a list of all the sites. What I don't see are my pages. Right? So how do we set up ownership of the pages and posts and things like that? So we want to use the relationships inside Directus.\u003C/p>\u003Cp>We're gonna set these up. We're gonna go to our sites. And again inside our data model that we spec'd out, pages belong to a single site. So one page belongs to one site. Same thing for posts and all the way down.\u003C/p>\u003Cp>So what we're going to do here is use the one to many field inside Directus. We're going to call this pages. The key is the name of the field inside the sites collection. We're gonna pick our related collection, which is gonna be pages, and then the foreign key will be the field that holds the site ID in the pages collection. So, you know, this might be site ID.\u003C/p>\u003Cp>I prefer just calling it site just because of the way that you can grab that relational data in a single API call. Alright, so the next thing we want to do is make sure we set up our display templates. And in this case, you know, I could go in and pick the fields that we wanna display, but what I'm gonna do here, I'm gonna use the table layout instead. So I could show multiple columns here and kinda get like a a standard table view that I'm looking for. So we'll add the title of the page, the status of the page, and do we need any more than that?\u003C/p>\u003Cp>Maybe not. Maybe we want permalink just because that could be dramatically different than the title. We will sort those by our sort order, and we're just gonna enable these other options to enable searching and to show a specific link. Okay. So now we've established a relationship to our pages.\u003C/p>\u003Cp>What does that actually look like? Alright. Let's scroll down. I can already see that the UI is getting a little bit, messy as far as user experience. We'll tackle that in just a moment.\u003C/p>\u003Cp>But here, now I can see my pages. I can create a new page directly from this site, or I can go in and add existing pages to that site. So I could say, okay. Hey. This site has a home page.\u003C/p>\u003Cp>It has a privacy policy page. I can go ahead and add these other pages to this specific site. There we go. I can save and stay, and then I've got, you know, the ability to edit these pages. I can also navigate directly to the page from there.\u003C/p>\u003Cp>So this is looking nice. One of the other things that I wanna do here though, because I care a lot about what my content editors are working with inside the CMS, we're gonna clean this up a bit. And I'm sure if you're watching this, you care as well. So there's an extension that I love to use quite a bit. It is called the group tabs group tabs interface.\u003C/p>\u003Cp>I've already got this installed in the simple website CMS template. It is by mister Hannes Kuttner, which is a a member of our core team. I think he actually built this before he joined the core team, though. Great little extension. Not sure why my images aren't displaying here.\u003C/p>\u003Cp>But basically, it gives me a nice tabbed interface. So what we're gonna do, we're gonna go to our sites data model, and I'm gonna look for the tab group option. Now this has got an option to actually fill the width of the page, so we'll take advantage of that, and I'm gonna save this. So this basically gives me a container where I can start dragging and dropping these other fields within, and we're gonna clean up this UI a bit. So let's see what that actually looks like.\u003C/p>\u003Cp>Boom. Now I've got title, I've got URL, I've got tagline. Except, you know, maybe I want to nest all of these fields under one single tab, right, instead of having, like, 35 different tabs. So what I can do here, I'm gonna reach for another field type here called the raw group. This doesn't actually create any columns in the database or anything like that.\u003C/p>\u003Cp>We're just gonna call this group branding, for instance. That's gonna be a key that we need to add, and then that gives us another container. And I could do things like this, where now I've got the title, I've got the URL, got the tagline, description, social links, and maybe we put pages up here ahead of those. So basically what I'm doing now is constructing the interface. So now we have pages.\u003C/p>\u003Cp>We have all these fields that show up under group branding. That doesn't doesn't seem right to me. Right? That looks kinda funky. When somebody who's adding this, they're gonna say group branding.\u003C/p>\u003Cp>What does that mean? So how can we control that? Right? The developer in me wants to say, hey. I need everything nicely name spaced and organized just for my own sanity, especially when I add more people to this.\u003C/p>\u003Cp>But the designer, the UX person in me says, hey. This this doesn't make sense. So what we can do is go into edit that specific field, and under the field settings, there are the field name translations. This is one of the most underrated features in Directus, I think. So I could just give this a translation.\u003C/p>\u003Cp>Right? And I pick the language. Of course, my user is set to the English language, but you could create translations for as many different languages as you have users. And Directus will display the correct one because when they log in, they can set up their default language on their profile. So I can work in English.\u003C/p>\u003Cp>Alexander on our team, he can work in French. Some of our fine folks from Germany, they can work in German, and so forth. Whatever it whatever we need. Alright. So as soon as I edit that, nothing changes here.\u003C/p>\u003Cp>But when I go back to the sites, now you can see that tab has a nice name that more aligns with what we're trying to achieve. Alright. So now that we have this relationship between pages, right, we keep moving on down the line. I'll go back to sites, and we're gonna add another one to many relationship for posts. That will be the related collection of posts, the foreign key, we're gonna use site.\u003C/p>\u003Cp>We'll add a table for this. And let's say we want the title of the post, so we want the status of the post, and we want when this was published. You know, typically, blog posts are rolling through in chronological order. So now for our sort field, instead of, like, an actual sort field, we're probably gonna wanna do published at. And maybe we do that by descending order so that we show, the most recent posts first.\u003C/p>\u003Cp>I can set how many pages or how many items per page that I wanna show in the table. And away we go, we can hit save. Great. So we'll add posts. Let's go back and hit one to many again.\u003C/p>\u003Cp>We're gonna do forms. So we'll do related collection forms. We will do the foreign key of site. Keep with that same theme. For the forms, do we actually need the table view?\u003C/p>\u003Cp>Yeah. Maybe we wanna add that. Right? So we've got title. We've got, the number of fields for the form.\u003C/p>\u003Cp>Maybe we wanna show that information. The number of fields, what fields we have. Over the fields we go. Laughing all the way. How many submissions maybe?\u003C/p>\u003Cp>Looks great. Cool. And now we'll do that. Now I know that we're gonna run into an issue with navigation because I'm the one who built this website CMS template and let me show you why. So when constructing this, the Symbol CMS template is made for a single website, not multi site, which we're building here.\u003C/p>\u003Cp>So the problem is the primary key is set up for a single site. Basically this is just a string. Right? Because I wanted to be able to do something like this for our actual content where I could say, hey, item slash navigation, and instead of calling, like, a random UUID, I could just call main and I can get my main navigation. And again, using that fields parameter, I could go in and get all the underlying items for that navigation.\u003C/p>\u003Cp>And I could go even further and do items dot children dot asterisk and get all of the children or even deeper. Right? Items.children.page. And then I can get the details of the actual page that we're linking to, like the permalink, so that I don't have to create brittle links that are gonna break as soon as soon as someone changes a permalink or a slug. Right?\u003C/p>\u003Cp>But in my haste in doing that, I kind of limited us here as far as what we can do with this structure. Right? Because if this is called footer, right, I would venture to guess that a lot of the sites that we're working on are also gonna have a footer. So then we have to worry about creating unique names. So what we're gonna do here, I'm just gonna trash this entire navigation collection.\u003C/p>\u003Cp>Boom. Away it goes. I could go into the actual SQL and update this, but for this purpose we'll just keep it nice and lightweight. We'll just create this collection again, And instead of using a string as the primary key, we're just going to use UUID. Perfect.\u003C/p>\u003Cp>We'll give this a key. And let's make this and give it a key field. Yeah, just to be clever. Alright. We can even set this field to be indexed for database performance.\u003C/p>\u003Cp>And maybe we want to slugify this thing as well so that it is friendly in URLs. The internal key for this navigation menu for this menu, I don't think we need to be super verbose here. I think there was an is active option there, so we'll just set that. Is this menu actually active for the site or not? And then I'm gonna go back and just like we did on sites, I'm gonna create a one to many relationship to our navigation items.\u003C/p>\u003Cp>So the items are gonna be the individual links. The navigation is basically our entire menu. So navigation will be main menu or header or footer, and then within that we'll have the navigation items. Great. Alright.\u003C/p>\u003Cp>For the foreign key is gonna be navigation. That's our parent collection. And you can always kinda see you can already see kind of the conventions that I follow where we have a singular and then a plural here for the the child relationship, just to try to keep things clean. Again, you could follow whatever convention that you want here. Totally up to you.\u003C/p>\u003Cp>So then we'll have a title for those items. We'll show a link. We'll sort by the actual sort field that's an integer. And if I expand this a little bit, you know, we can go in and set up our display templates, which we'll wanna do just to make sure that, when we're working inside, like, a a layout, which is like the table or the kanban or whatever layout you prefer to edit this, that that information shows, the actual title instead of random UUIDs. Alright.\u003C/p>\u003Cp>So, we're getting close there. Let's just add a menu icon for this and put this back up into our list. We'll put navigation items below that. Great. And now we can resume what we were doing.\u003C/p>\u003Cp>Go back to sites. We'll do a one to many relationship here. This will be navigation. Could call it menus. That's the hardest part of doing any of these things is what to call the names.\u003C/p>\u003Cp>But we'll just follow the same convention. Navigation. There's the site. Here's the menus. We're gonna show the key.\u003C/p>\u003Cp>Maybe we wanna show the number of items there. Great. We're gonna sort by nothing. Great. Alright.\u003C/p>\u003Cp>Now let's take a look at our progress. I'm kinda curious where we're at on the timer. We're about half an hour in. Still got a good way to go. But as far as what we're working with, and I can already see, right, there's our display templates that we need to set up.\u003C/p>\u003Cp>So I'm seeing basically the UUID of all the pages, and that's not what I wanna see. Right? So I'm gonna just go to pages here. You can see I've got my interface, which is the actual form detail. That's where these things show.\u003C/p>\u003Cp>The display shows up in that layout, so the the list of all the data. Right? We're gonna choose to show the related values, and I can add whatever fields here I want from pages. So maybe the title and the status of that page. Great.\u003C/p>\u003Cp>We could do the same thing for our blog posts. We wanna show the title and the status. Maybe the same thing for the form. We just wanna show the title of the form. And for our navigation, maybe we want to show the key and the number of items.\u003C/p>\u003Cp>Let's see how that looks. Right. Now we get something that's a little more manageable. We can see, hey, here's all the pages. I can bounce out to a specific page right from this view.\u003C/p>\u003Cp>I can go into the actual site. There's the name for this site. We can see we've got pages. We've got posts. We've got forms.\u003C/p>\u003Cp>We've got navigation. We've got branding. This is starting to look wonderful. Right? We'll probably need a header for this site.\u003C/p>\u003Cp>Let's see if we can copy some of these existing items. That's great. Let's create a new one. We'll call this the footer. Add some of these other items that we did not copy, contact us, privacy policy, blog, etcetera.\u003C/p>\u003Cp>Great. Make that active. We'll make the header active. Great. We hit save and stay.\u003C/p>\u003Cp>We can see okay. Here's the items within each one of those. Perfect. Everything's looking nice. Great.\u003C/p>\u003Cp>So now we have this site structure. Right? And if I were to go to my Directus URL, I go to items slash sites, I can get all the info for the site. Site b is looking pretty lonely in this case. We'll sort that in a moment.\u003C/p>\u003Cp>But here, there's lots of ways that I could get access to the information for this specific site. Again, I could go through and use the fields params here to fetch all the pages for the site. So pages dot asterisk, and I could see here's the same content I was getting at the pages endpoint. Now I could also, like if I were to grab the ID of this specific site, right, I could go through and get all the pages. So, we can see here there's a a site field where we're storing an actual ID.\u003C/p>\u003Cp>That's great. Let's go in and how do we get back to the actual admin? Admoine. Sausage fingers get me every single time. But let's go in and add a page to site b.\u003C/p>\u003Cp>Right? So we have site b. We're gonna create a new page for site b. Site b page. Cool.\u003C/p>\u003Cp>We'll just go ahead and pretend like this is a great page. We'll publish it. Cool. And now if I go back and I refresh, right, we're seeing this page for site b. So how can I filter this out?\u003C/p>\u003Cp>I could use our filter params for that. So I could do, like, filter. We're gonna do the site, we're gonna use underscore equals, which is our syntax for that. And oh, I lost my fancy stuff. So we're gonna run that again.\u003C/p>\u003Cp>Filter site. Just wrap these in brackets equals this. And now you can see I only see the items for the agency OS site, and I don't see that page that I just created. Now this is great for us in a multi site CMS setup. Right?\u003C/p>\u003Cp>I can have all this content, I can query it by the individual site, but let's talk about the problem here. So the problem is how do we lock down this content? So I'm just gonna copy this. Right? I'm gonna pull this up in another window, an incognito window.\u003C/p>\u003Cp>We're gonna discard the changes here for site b. And I'm gonna go in and add our first user. So the scene is set. We've got our sites. We want our users to be able to start editing the content.\u003C/p>\u003Cp>Alright. So this is gonna be our agency OS content editor. Great. This will be agencyOS@example.com. We're gonna do a real secure password of password.\u003C/p>\u003Cp>Alright. So I'm gonna create this new user. I could also invite users. I'm gonna give them the role of team member, which has already has a couple policies, attached to it. So if we just look at that, we see that this person can they'll have access to the direct to studio.\u003C/p>\u003Cp>They'll have, edit capabilities for our specific content. Right? So we'll just create this new user, and then I should be able to log in as that user over here. Example.com. Password.\u003C/p>\u003Cp>Badda bing badda boom. And now I can see all of these pages. Right? And you're probably already picking up on the problem. We could just make it more visible here.\u003C/p>\u003Cp>If we drag site over, that's gonna aggravate me. So we're just gonna go ahead and fix that in the pages section. Alright. I can go to site. I wanna display the related values.\u003C/p>\u003Cp>I wanna show the name of the site. In the mini to one, we're gonna show the name of the site. Great. Great. Beautiful.\u003C/p>\u003Cp>I can unhide this field on the pages if I want to. I'll show you what that looks like in just a moment. But now if I just refresh over here, you can see the problem. Well, number one, I don't have access to see any of the sites, but I can see the content for site b. And I can edit this even though I am not the content editor for site b.\u003C/p>\u003Cp>So let's just go in and fix these permissions. Looks like I've got a few that we need to fix. We'll just add site access. And we'll just set all of these up for all right now just to get back to, healthy baseline of this. We don't want folks to create sites.\u003C/p>\u003Cp>We don't want them to delete sites. We're gonna leave that for the admin. And then I'll just hit save and stay. Alright. So now I'll be able to see sites up here.\u003C/p>\u003Cp>And again, you can see the problem is that I could go in and I could change the title of this to Site B Sucks. Not cool. So how do we actually lock this down from being edited? Right? Directus gives us a great way to do that with custom permissions.\u003C/p>\u003Cp>And this is a little complicated the first time that you encounter it, but once you understand what's happening, it is incredibly powerful. So here we are on our access policy for content editors. Right? Anybody with this policy attached to their user directly or to their user role will have edit access for any of these things that we've set up. And these are just the individual collections.\u003C/p>\u003Cp>Right. And one way that Directus protects your data when you add a new collection, we don't give access rights to any of the those new collections by default. We like to keep things secure. But, the AgencyOS folks should not be able to see content for Site B. So what do we need to do?\u003C/p>\u003Cp>Right? We need to establish a relationship between site and the user. So to do that, we're gonna go back to our sites, and in this case we're gonna use that many to many relationship that we already talked about. We're gonna create a new field inside the sites collection called users. The related collection will be direct us users.\u003C/p>\u003Cp>We don't want to allow duplicates. We're gonna show a link. Great. We'll just go ahead and create this. Actually, I'm gonna delete that back up.\u003C/p>\u003Cp>Forgot to explain what's actually going on. In that many to many relationship, basically it creates a junction table for us. So let's try again. We'll do sites, our users, direct us users is the related collection. And instead of hitting save here, I'm gonna open up this advanced mode, and now you can see what's going on underneath the hood.\u003C/p>\u003Cp>Right? There's a junction collection that's being created inside our database. I'm gonna call this siteusers. By default this will get auto populated. You can go in and change it.\u003C/p>\u003Cp>Right. So I'm gonna clean up this convention just a little bit. So we've got the junction table will be siteusers, the site ID will be stored under site, the directus user ID will be stored as user, and then one important piece here is to add this corresponding field to the directus users collection as well, because we're gonna need to use that in our permissions. So we'll add a sort field, and the relational triggers here just let us set up, actions based on what happens to this specific thing, right. If we deselect the site users or we delete the sites, we delete one of the directest users, we probably wanna keep this thing clean and delete the record inside the junction table.\u003C/p>\u003Cp>Great. Alright. So what did we actually achieve here? So if I go into sites, we go to AgencyOS, I can now add that AgencyOS content editor to that specific site. Okay.\u003C/p>\u003Cp>Great, Bryant. Why can he still edit all of the content in site b? That's where our access policies come into play. Right? We're running on twenty minutes here.\u003C/p>\u003Cp>We still got plenty of time to set this up. What we're going to do now is restrict access to all of our content. So we've got all the relationships set up. We're ready to lock this down. What we're gonna do, we're gonna go into our sites.\u003C/p>\u003Cp>Right? The first thing that we wanna lock down if you're not part of site b, you shouldn't be seeing site b. Right? So we'll go into our read access. This has full read access.\u003C/p>\u003Cp>We're gonna use custom permissions to scope this down a bit. So we're gonna drill into our users, and this is our junction table, right? So the user, users dot users equals dollar sign underscore current user. So basically what we're saying is if the user is part of this site and they are then able to read information from that site. So why is that not showing up?\u003C/p>\u003Cp>Oh, we probably didn't set up permissions for the junction table. There we go. Okay. We don't want anybody to be able to adjust that. Okay.\u003C/p>\u003Cp>So now I can see all the users within a certain site for only sites that I am a part of. Right? So I can no longer see or search or find Site B for this example user that's logged in. Right? The AgencyOS Content Editor, they cannot see Site B.\u003C/p>\u003Cp>Now if I go into site b and I give them access to site b and adding them as a user of site b, refresh over here, boom, site b shows up. I remove them. You can now not get access to site b. Right? One problem though, while I don't have access to the actual site, I can still see the pages for site b.\u003C/p>\u003Cp>So I here's where a bit of the tedious part comes in, where I just go in and actually edit our permissions for all of these different items. Right? So we wanna be able to edit, create new pages, but when it comes to reading and updating pages, we want to make sure that those are scoped properly. So the site, we're gonna go back to that junction table. Right?\u003C/p>\u003Cp>Site users, user equals current user. So because we have a relationship to the site from pages, we are now able to check all the users within the site and make sure that the ID for those includes the current user ID. And what I'm gonna do here, I'm gonna just copy this rule, and I'm gonna go through and paste this rule into the item's permissions item permissions. And I could go through and do that for other things as well, like our post collection. Where are you posts?\u003C/p>\u003Cp>Right? Copy paste this rule. Again, we'll add the rule there. We don't want them to be able to delete pages that don't belong to their site. Same thing for posts.\u003C/p>\u003Cp>And, you know, depending on your scenario, right, whether you're setting up multi site for different clients or different teams within a larger organization or, yeah, some combination thereof, you've got total control of this by creating different access policies, different roles. So the access policies I love, because I can get really granular about what what content I want to make available to read, update, create, etcetera. Alright. So for the sake of time, I'm not gonna go through and adjust all of these things, but if I just hit save and stay, now I should not be able to edit any content for site b or even see it unless I'm part of that site. And that applies for the API as well, right.\u003C/p>\u003Cp>So if I go in and I do items slash pages, we make this pretty print, I don't see any of those pages. Right? And just to prove this to you, we've got a page here that belongs to site b. If I copy the ID of this, and remember I'm logged in as the content editor for site, AgencyOS over here, right, I'm gonna get a error that I do not have permission to access this specific page. So great.\u003C/p>\u003Cp>That works really nicely, Bryant. Cool. How do we actually implement this on a, like, front end scenario? Right? How should we set this up?\u003C/p>\u003Cp>And one pattern that I follow quite a bit is creating a bot user for my front end. Right? So now that we've got this data model set up, everything is flowing permission wise the way that we want it, right we would go through and create our front ends. So you could have your front ends all in Next. Js, Nuxt, whatever front end framework you prefer.\u003C/p>\u003Cp>You can even have different front end frameworks. You're gonna wanna fetch that content and either generate your front end statically, dynamically, server side rendered, incremental static regeneration, all the different ways of rendering your content, you know, all available to you via the API. But one pattern that I like, I will go in. I'm just gonna create a new role. I'm gonna call it bot.\u003C/p>\u003Cp>And this is just a role for our website front ends to access the API. Alright, so within roles we are going to add content policies. Right? So we don't want our front end to actually be able to edit the content, we just want it to read content. Potentially we might want to let it upload files or submit forms.\u003C/p>\u003Cp>But for now, let's just keep this simple. We are going to let it read content. And one of the other roles that comes with Directus out of the box is the public role. So the public role controls what API data is available without authentication. Now usually I will take advantage of the public role, and you can see we have different policies here.\u003C/p>\u003Cp>So I can let the general public read all the published content. That's okay. In this multi tenant scenario, you might want to lock that down. Right? So I'll just remove all access for the public role.\u003C/p>\u003Cp>And if I get really crazy with this, go to Safari, we go to items, we go to pages, right, now I don't get access to any of the content. Right? I cannot have access to pages. But, if I am still logged in over here, we can get access to our pages. Right.\u003C/p>\u003Cp>Right. And, again, we're only seeing the page data for agent c o s. Cool. So we've created this bot user or this bot role. Let's take a look at our read policies.\u003C/p>\u003Cp>We'll just go ahead and fix all of these other collections that we would need to render on the front end. Probably the navigation. Great sites. Cool. And again, you can see some of these are already, kind of partial.\u003C/p>\u003Cp>50% opacity here just highlighted where it says partial read access. Let's take a look at, like, pages for instance. I'm gonna save this really quickly though. So let's take a look at pages. Right?\u003C/p>\u003Cp>The rule for pages is that it has to be published, and the published date has to be now or before now. Makes sense, right? But we wanna add one more stipulation to this that any of these bot users can only access the sites that they have access to. So again, we can use that same setup where we go into the Junction collection, we use the current user. And with these bot users, I would highly recommend just creating one bot per website.\u003C/p>\u003Cp>So if you've got 50 websites, you create 50 bot users, that will be able to give you a token that will only, be able to access data for that specific site. And that way you don't have to keep passing something like site ID equals x y z in the parameters or every single call. Because that can get tedious, honestly. So here we're gonna adjust posts to do the same thing. We want the site, we want the underscore users user equal current user.\u003C/p>\u003Cp>And again, this is just a dynamic variable. Directus will populate that user ID for you there. Did we do that for pages as well? Let's do it for pages. Where are you?\u003C/p>\u003Cp>Sites. Tabs, users. Current user. Great. And if I want to, like, copy and paste these entire rules, these are just the standard filter rule syntax.\u003C/p>\u003Cp>If you check out docs.directus.io, you can find all of these things. Right. Great. Okay. So now we've got that locked down.\u003C/p>\u003Cp>Let's go in and create a new bot. Right? Let's say bot, site, site b bot. Crack myself up sometimes. And here, like, basically, we don't need an email and password.\u003C/p>\u003Cp>These folks aren't gonna log in to the data studio. Right? This is just gonna be a user for our front end to access the data. So in this case, we're gonna add that bot role. Cool.\u003C/p>\u003Cp>So I can know how many of these bots that we have. One other thing to note is that I don't even have to create a separate role. I could just give them read access, through the policies. So you've got a lot of flexibility there, and, you know, maybe you've got that one content editor who needs to do a little bit more than the others. Instead of having to create a separate role for them, just give them the policy that they need.\u003C/p>\u003Cp>What I'm gonna do here is create a static token. This just allows us to access it and we're gonna add this bot to Site B. Alright. Cool. Great.\u003C/p>\u003Cp>Save. Now we've got this user. And, cool. So now, what are we gonna do? I want to just demonstrate this.\u003C/p>\u003Cp>I don't think we're gonna have enough time. We've got like ten minutes before the hour's up. We're probably not gonna be able to dive into anything front end for that'll be tremendously helpful. Right? But let's just pretend like we're doing this on the front end.\u003C/p>\u003Cp>Documents. Great. We'll create a multi site collection here. I'm just using this HTTP client called Bruno. Usually, we do not talk about Bruno, but in this case, we are talking about Bruno.\u003C/p>\u003Cp>So I will grab the URL here from our API, and I'm gonna pretend to be the site b front end here. So we're gonna try to fetch our pages for site b. If I just make this full screen, I should get an error. Right? We do not have permission to access this.\u003C/p>\u003Cp>Rightfully so, we disabled all public access. But now if I go in and I add my token, right, now without having to add a param for site or or any of this mess. Right? I am only getting the content for site b because of the relationships and the permissions that I set up. So this bot user is a part of site b, and we can fix that to actually display this.\u003C/p>\u003Cp>So we'll go into direct us users. We'll go to sites. We want to display the site name. We'll display the site name in our display templates. Great.\u003C/p>\u003Cp>So now if we go back to bot for site b, alright, we can see the sites that they're available to. Now, if you're creating a lot of sites, you know, in this process of creating bots for each site are kind of cumbersome. Anytime you create a new site you could potentially run a flow that creates this bot user automatically, etcetera, so that you don't have to mess with this. It just depends on how many different sites that you're managing and what that setup kinda looks like. But here you can already see, let's go in and, you know, if I add the bot to the AgencyOS site, I save that.\u003C/p>\u003Cp>And if I rerun this API call, you can see now I'm getting all of those pages for the AgencyOS site as well, which is not what we want in this case. But, that way we can lock down the data that each one of these folks have access to. And this could be an unlimited number of front ends. So I hope that is it for this one. That is creating a multi site CMS.\u003C/p>\u003Cp>Right? Just to recap, we've got a sites collection that we created. We shifted all of our settings for the site into the actual sites collection themselves. Then we have relationships for pages, posts, etcetera, and we set up a bot user to access that for our front end. Right?\u003C/p>\u003Cp>On the front end, we would then just set up an API call to the direct us API. Not gonna get into that. That is gonna be opening a can of worms with five minutes left, and I don't wanna disappoint myself. So with that, multisite CMS, we're calling that a wrap. That's a win for this episode of 100 apps, one hundred hours.\u003C/p>\u003Cp>I hope you'll tune in to the next one, and make sure you check out our documentation for more insight on how to set some of these things up. I'll see you.\u003C/p>","Welcome back to the next episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. And today, we are going to be building a multi site CMS. It is been impossible to ignore the WordPress drama over the last month or two, and that has left a lot of folks wondering, hey, should I be switching our CMS from WordPress? WordPress has the multi site mode, so we're going to walk through how to build that today with Directus. If you are new to 100 apps one hundred hours, there are only two rules. Number one, sixty minutes to plan and build, nothing more, nothing less. Everyone knows the rules. And, the second rule, the anti rule, one that I'm definitely going to invoke today, use whatever you have at your disposal. So let's dive right in. We're gonna start the clock. Sixty minutes on the timer. Let's talk about multi site CMS. So what are we trying to do here? We want to manage content for multiple sites. That could be two, that could be three, it could be 25, it could be 50 from a single CMS instance. So we will share some of our data models amongst those sites, but the actual content will be different. So, as far as the functionality that we're looking for out of the system, let's just mock that up. Functionality. Right? We want to be able to manage pages and posts and other types of content for multiple sites within one CMS. We wanna make sure that there, are users can only edit and access data for sites that they are assigned to, owners of, assigned to, etcetera. That's the main functionality that we are looking for. Right? So one of the other things that I always do on these is basically map out our data model. Right? And if we're going after WordPress here, we have, like, pages and we typically have posts. Those are kind of the two pieces of content that, you know, when I think of WordPress, I think of, okay, here's what's inside, the data model for that. But we're also gonna need to go up to a higher level. Right? Because each page, each post is going to belong to a certain site. Now in this scenario, I'm thinking each site or each page only belongs to a single site and if you need to duplicate a page for another site, you just create a a entire copy of it. But, you know, you could potentially set this up if you are sharing the same page across multiple sites. You could model that relationship as well. You know, that's one of the advantages of Directus as a CMS is that you can model your data however you want, and your table structure inside your database will follow suit. Directus just makes those edits for you. And then, you know, we're going to need a relationship to users. Right? So users are going to be kind of a mini to mini relationship. Right? Love drawing these arrows. We need an arrow over here. And, let's just say mini to mini. Right? One user could be working on three, four, five sites or could be a single site, and, you know, we could have many users working on the same site. Alright. So as far as our data model, that's that's pretty good. You know, we'll dive into the nitty gritty details in just a moment, but I'm definitely gonna invoke rule number two here, and I'm going to go into Directus Cloud. And you can call this cheating. You can call this taking advantage of what we've already built previously. But we are going to do a new project using our simple CMS template because it gives us most of the functionality we need right out of the gate. So we're gonna call this multi site CMS test or 100 apps. That's good. See how long we can get the URL for this particular project. And then I'm just gonna click simple CMS here. I'm gonna switch this to monthly, and I'm gonna check out. And this will spin up our project for us. So, if we think more along the lines here of of what we actually need to do, You know, this simple CMS template also has forms. It has navigation, and I'll show you that in just a moment after we get it fired up. And then we have a global settings collection, inside there, which is actually, we're gonna have to do a bit of work to that. Can't get my arrows straight here. Jeez. Okay. There we go. Alright. So those all roll up to the sites. There are many users, many sites. We're just gonna wait for this Directus instance to spin up. And I'm assuming I've probably already got an email with this thing. Great. We'll just copy this. And feel free to steal this password here because this project should be turned off by the time we get to airing this actual episode. Alright. So we're just waiting for the CDN. Are we ready yet? We are ready. Okay. So great. Now we've got our login. Let's just go ahead and fire this up. We'll get logged into our simple CMS instance. And let's just kind of go through this actual CMS. I'll zoom way in and we can kind of dive through the content here. So we've already got some pages. We've got posts. We have forms, form submissions, navigation. So within pages, we can see we've got standard stuff, title, a permalink, what the status is. We've got some blocks here, of different content. So this is using the mini to any relationships inside Directus. I can drag and drop this to build a page dynamically, which is nice for our content editors or the marketing folk that are actually working on the content for the site. Then we have our blog posts. So lots of bunny related content here. We've got slugs. We've got status. Is this published? Yes or no? We got description, author, actual content. There's some AI features baked into this template. It is, a really nice starter kit for any Directus project if that's what you're looking to do. We got form fields. Great. There's a newsletter sign up, we need a first name, we need an email address, etcetera. And then, you know, every site needs a menu. So here we have some navigation, pretty cut and dry. And then there's the globals collection where we're storing settings that are going to be per site in our specific use case like logo, favicon, favicon, five icon. Great. Never know how to say that one. And we will dive in. Right? So we've got pages. We've got, you know, I could go in and query this particular content. So if I just drop this admin off the URL and I go to pages, we could query this content. If I wanted to see the individual content of the blocks, I can add a fields param to this and just do an asterisk, which will essentially act as a wildcard. It'll give me all the root level fields, and then I'm gonna say blocks, and I could do blocks dot collection, blocks dot item, And I can see I can start to fetch all the content within, and and I could keep drilling into this if I wanted to. But that doesn't cover our use case of multisite. Right? So the big glaring problem with all of this is we don't have sites. So let's go in, we're going to create our sites collection. We'll go to the settings, we'll just create sites. I'm going to use a generated UUID for this. We can add some of these in the canned system fields that add functionality like timestamps and users automatically. I'm just gonna leave this blank for now. And we're gonna give this site a name. And let's add a little hint for the user here. So I'm going to go to Advanced Field Creation Mode. I'm going to go to the Field tab and we're just going to say the internal name for this site. Great. That'll give us a hint. And voila, now we have sites. I'm gonna give this a nice looking icon, maybe a globe. That stands for, like, website. Right? We'll drag this up to the very tippy top. I've got globals as well. Maybe we need to change the icon. Let's do, like, settings or something. I used to be a designer. I like to refer to myself as a recovering designer, so, icons and user experience and small little details like that matter a lot to me. So let's go ahead and create a new site. Right? We're gonna do Agency OS. That's one of our other, open source projects that we released. And this is gonna be site b. Right? The other site. The site that I don't like. Site b. Don't even care about it enough to give it a nice name. Alright, so now what do we do with these specific sites? Right? Now that we have a site, maybe we want to go in and establish the relationship between all the different pages and things. Let's start with our globals though. Alright? These are settings, like the title, the URL, the logo, the favicon. Some of these we're gonna use for SEO. Some of them, maybe in the the footer like social links, things like that. We want to have these per site. Alright? So one of the nice things about Directus is this visual data modeling. I can come in and I can copy these fields across collections. So we will go to sites. We're gonna create a duplicate of that title field. I'm gonna duplicate the URL. I could even get fancy if I wanted to and potentially create, like, a flow to duplicate these things. You'll notice here on the ones that are relational, like, this is based on a many to one relationship. I cannot duplicate those fields just because the underlying schema changes that happen. But just regular any other fields that are not relational, I can easily duplicate. So we'll do that. We'll add our description. Copy that over. And let's do our social links. Now you'll also see there's this credentials group, like our open AI API key. Gosh. That's a mouthful. I wish, I wish their name was a little bit different because I'm dealing with these API keys in a lot of episodes, but I digress. So these things are probably gonna be they're gonna stay global. Right? We don't actually need to remove those. So here, let's just remove title. Let's remove URL now that we've copied it. The logo is just a nice little divider there. We'll remove the favicon. We'll delete all of these fields that are no longer global in our data model. And today's exes episode is is really just a, a big exercise in data modeling. Alright. So now if I go to the globals, all we have is the direct Us URL. So we're gonna serve all the the content for those sites out of a single instance. That's good. You know, they would probably share an API key or, you know, we could set it up for where that API key is per site if we wanted to. But now we can see we have these things, like, agency OS, the best operating system for your agency. Alright. We'll give this a URL, agencyos.dev. Run your business better. Alright. We can add social links. Great. Cool. Gravy. Right? And now, I could go in and, you know, I just got this option here to copy the API URL, and now I could see that individual site. So this is just my directus URL slash items slash site slash ID, or I can get a list of all the sites. What I don't see are my pages. Right? So how do we set up ownership of the pages and posts and things like that? So we want to use the relationships inside Directus. We're gonna set these up. We're gonna go to our sites. And again inside our data model that we spec'd out, pages belong to a single site. So one page belongs to one site. Same thing for posts and all the way down. So what we're going to do here is use the one to many field inside Directus. We're going to call this pages. The key is the name of the field inside the sites collection. We're gonna pick our related collection, which is gonna be pages, and then the foreign key will be the field that holds the site ID in the pages collection. So, you know, this might be site ID. I prefer just calling it site just because of the way that you can grab that relational data in a single API call. Alright, so the next thing we want to do is make sure we set up our display templates. And in this case, you know, I could go in and pick the fields that we wanna display, but what I'm gonna do here, I'm gonna use the table layout instead. So I could show multiple columns here and kinda get like a a standard table view that I'm looking for. So we'll add the title of the page, the status of the page, and do we need any more than that? Maybe not. Maybe we want permalink just because that could be dramatically different than the title. We will sort those by our sort order, and we're just gonna enable these other options to enable searching and to show a specific link. Okay. So now we've established a relationship to our pages. What does that actually look like? Alright. Let's scroll down. I can already see that the UI is getting a little bit, messy as far as user experience. We'll tackle that in just a moment. But here, now I can see my pages. I can create a new page directly from this site, or I can go in and add existing pages to that site. So I could say, okay. Hey. This site has a home page. It has a privacy policy page. I can go ahead and add these other pages to this specific site. There we go. I can save and stay, and then I've got, you know, the ability to edit these pages. I can also navigate directly to the page from there. So this is looking nice. One of the other things that I wanna do here though, because I care a lot about what my content editors are working with inside the CMS, we're gonna clean this up a bit. And I'm sure if you're watching this, you care as well. So there's an extension that I love to use quite a bit. It is called the group tabs group tabs interface. I've already got this installed in the simple website CMS template. It is by mister Hannes Kuttner, which is a a member of our core team. I think he actually built this before he joined the core team, though. Great little extension. Not sure why my images aren't displaying here. But basically, it gives me a nice tabbed interface. So what we're gonna do, we're gonna go to our sites data model, and I'm gonna look for the tab group option. Now this has got an option to actually fill the width of the page, so we'll take advantage of that, and I'm gonna save this. So this basically gives me a container where I can start dragging and dropping these other fields within, and we're gonna clean up this UI a bit. So let's see what that actually looks like. Boom. Now I've got title, I've got URL, I've got tagline. Except, you know, maybe I want to nest all of these fields under one single tab, right, instead of having, like, 35 different tabs. So what I can do here, I'm gonna reach for another field type here called the raw group. This doesn't actually create any columns in the database or anything like that. We're just gonna call this group branding, for instance. That's gonna be a key that we need to add, and then that gives us another container. And I could do things like this, where now I've got the title, I've got the URL, got the tagline, description, social links, and maybe we put pages up here ahead of those. So basically what I'm doing now is constructing the interface. So now we have pages. We have all these fields that show up under group branding. That doesn't doesn't seem right to me. Right? That looks kinda funky. When somebody who's adding this, they're gonna say group branding. What does that mean? So how can we control that? Right? The developer in me wants to say, hey. I need everything nicely name spaced and organized just for my own sanity, especially when I add more people to this. But the designer, the UX person in me says, hey. This this doesn't make sense. So what we can do is go into edit that specific field, and under the field settings, there are the field name translations. This is one of the most underrated features in Directus, I think. So I could just give this a translation. Right? And I pick the language. Of course, my user is set to the English language, but you could create translations for as many different languages as you have users. And Directus will display the correct one because when they log in, they can set up their default language on their profile. So I can work in English. Alexander on our team, he can work in French. Some of our fine folks from Germany, they can work in German, and so forth. Whatever it whatever we need. Alright. So as soon as I edit that, nothing changes here. But when I go back to the sites, now you can see that tab has a nice name that more aligns with what we're trying to achieve. Alright. So now that we have this relationship between pages, right, we keep moving on down the line. I'll go back to sites, and we're gonna add another one to many relationship for posts. That will be the related collection of posts, the foreign key, we're gonna use site. We'll add a table for this. And let's say we want the title of the post, so we want the status of the post, and we want when this was published. You know, typically, blog posts are rolling through in chronological order. So now for our sort field, instead of, like, an actual sort field, we're probably gonna wanna do published at. And maybe we do that by descending order so that we show, the most recent posts first. I can set how many pages or how many items per page that I wanna show in the table. And away we go, we can hit save. Great. So we'll add posts. Let's go back and hit one to many again. We're gonna do forms. So we'll do related collection forms. We will do the foreign key of site. Keep with that same theme. For the forms, do we actually need the table view? Yeah. Maybe we wanna add that. Right? So we've got title. We've got, the number of fields for the form. Maybe we wanna show that information. The number of fields, what fields we have. Over the fields we go. Laughing all the way. How many submissions maybe? Looks great. Cool. And now we'll do that. Now I know that we're gonna run into an issue with navigation because I'm the one who built this website CMS template and let me show you why. So when constructing this, the Symbol CMS template is made for a single website, not multi site, which we're building here. So the problem is the primary key is set up for a single site. Basically this is just a string. Right? Because I wanted to be able to do something like this for our actual content where I could say, hey, item slash navigation, and instead of calling, like, a random UUID, I could just call main and I can get my main navigation. And again, using that fields parameter, I could go in and get all the underlying items for that navigation. And I could go even further and do items dot children dot asterisk and get all of the children or even deeper. Right? Items.children.page. And then I can get the details of the actual page that we're linking to, like the permalink, so that I don't have to create brittle links that are gonna break as soon as soon as someone changes a permalink or a slug. Right? But in my haste in doing that, I kind of limited us here as far as what we can do with this structure. Right? Because if this is called footer, right, I would venture to guess that a lot of the sites that we're working on are also gonna have a footer. So then we have to worry about creating unique names. So what we're gonna do here, I'm just gonna trash this entire navigation collection. Boom. Away it goes. I could go into the actual SQL and update this, but for this purpose we'll just keep it nice and lightweight. We'll just create this collection again, And instead of using a string as the primary key, we're just going to use UUID. Perfect. We'll give this a key. And let's make this and give it a key field. Yeah, just to be clever. Alright. We can even set this field to be indexed for database performance. And maybe we want to slugify this thing as well so that it is friendly in URLs. The internal key for this navigation menu for this menu, I don't think we need to be super verbose here. I think there was an is active option there, so we'll just set that. Is this menu actually active for the site or not? And then I'm gonna go back and just like we did on sites, I'm gonna create a one to many relationship to our navigation items. So the items are gonna be the individual links. The navigation is basically our entire menu. So navigation will be main menu or header or footer, and then within that we'll have the navigation items. Great. Alright. For the foreign key is gonna be navigation. That's our parent collection. And you can always kinda see you can already see kind of the conventions that I follow where we have a singular and then a plural here for the the child relationship, just to try to keep things clean. Again, you could follow whatever convention that you want here. Totally up to you. So then we'll have a title for those items. We'll show a link. We'll sort by the actual sort field that's an integer. And if I expand this a little bit, you know, we can go in and set up our display templates, which we'll wanna do just to make sure that, when we're working inside, like, a a layout, which is like the table or the kanban or whatever layout you prefer to edit this, that that information shows, the actual title instead of random UUIDs. Alright. So, we're getting close there. Let's just add a menu icon for this and put this back up into our list. We'll put navigation items below that. Great. And now we can resume what we were doing. Go back to sites. We'll do a one to many relationship here. This will be navigation. Could call it menus. That's the hardest part of doing any of these things is what to call the names. But we'll just follow the same convention. Navigation. There's the site. Here's the menus. We're gonna show the key. Maybe we wanna show the number of items there. Great. We're gonna sort by nothing. Great. Alright. Now let's take a look at our progress. I'm kinda curious where we're at on the timer. We're about half an hour in. Still got a good way to go. But as far as what we're working with, and I can already see, right, there's our display templates that we need to set up. So I'm seeing basically the UUID of all the pages, and that's not what I wanna see. Right? So I'm gonna just go to pages here. You can see I've got my interface, which is the actual form detail. That's where these things show. The display shows up in that layout, so the the list of all the data. Right? We're gonna choose to show the related values, and I can add whatever fields here I want from pages. So maybe the title and the status of that page. Great. We could do the same thing for our blog posts. We wanna show the title and the status. Maybe the same thing for the form. We just wanna show the title of the form. And for our navigation, maybe we want to show the key and the number of items. Let's see how that looks. Right. Now we get something that's a little more manageable. We can see, hey, here's all the pages. I can bounce out to a specific page right from this view. I can go into the actual site. There's the name for this site. We can see we've got pages. We've got posts. We've got forms. We've got navigation. We've got branding. This is starting to look wonderful. Right? We'll probably need a header for this site. Let's see if we can copy some of these existing items. That's great. Let's create a new one. We'll call this the footer. Add some of these other items that we did not copy, contact us, privacy policy, blog, etcetera. Great. Make that active. We'll make the header active. Great. We hit save and stay. We can see okay. Here's the items within each one of those. Perfect. Everything's looking nice. Great. So now we have this site structure. Right? And if I were to go to my Directus URL, I go to items slash sites, I can get all the info for the site. Site b is looking pretty lonely in this case. We'll sort that in a moment. But here, there's lots of ways that I could get access to the information for this specific site. Again, I could go through and use the fields params here to fetch all the pages for the site. So pages dot asterisk, and I could see here's the same content I was getting at the pages endpoint. Now I could also, like if I were to grab the ID of this specific site, right, I could go through and get all the pages. So, we can see here there's a a site field where we're storing an actual ID. That's great. Let's go in and how do we get back to the actual admin? Admoine. Sausage fingers get me every single time. But let's go in and add a page to site b. Right? So we have site b. We're gonna create a new page for site b. Site b page. Cool. We'll just go ahead and pretend like this is a great page. We'll publish it. Cool. And now if I go back and I refresh, right, we're seeing this page for site b. So how can I filter this out? I could use our filter params for that. So I could do, like, filter. We're gonna do the site, we're gonna use underscore equals, which is our syntax for that. And oh, I lost my fancy stuff. So we're gonna run that again. Filter site. Just wrap these in brackets equals this. And now you can see I only see the items for the agency OS site, and I don't see that page that I just created. Now this is great for us in a multi site CMS setup. Right? I can have all this content, I can query it by the individual site, but let's talk about the problem here. So the problem is how do we lock down this content? So I'm just gonna copy this. Right? I'm gonna pull this up in another window, an incognito window. We're gonna discard the changes here for site b. And I'm gonna go in and add our first user. So the scene is set. We've got our sites. We want our users to be able to start editing the content. Alright. So this is gonna be our agency OS content editor. Great. This will be agencyOS@example.com. We're gonna do a real secure password of password. Alright. So I'm gonna create this new user. I could also invite users. I'm gonna give them the role of team member, which has already has a couple policies, attached to it. So if we just look at that, we see that this person can they'll have access to the direct to studio. They'll have, edit capabilities for our specific content. Right? So we'll just create this new user, and then I should be able to log in as that user over here. Example.com. Password. Badda bing badda boom. And now I can see all of these pages. Right? And you're probably already picking up on the problem. We could just make it more visible here. If we drag site over, that's gonna aggravate me. So we're just gonna go ahead and fix that in the pages section. Alright. I can go to site. I wanna display the related values. I wanna show the name of the site. In the mini to one, we're gonna show the name of the site. Great. Great. Beautiful. I can unhide this field on the pages if I want to. I'll show you what that looks like in just a moment. But now if I just refresh over here, you can see the problem. Well, number one, I don't have access to see any of the sites, but I can see the content for site b. And I can edit this even though I am not the content editor for site b. So let's just go in and fix these permissions. Looks like I've got a few that we need to fix. We'll just add site access. And we'll just set all of these up for all right now just to get back to, healthy baseline of this. We don't want folks to create sites. We don't want them to delete sites. We're gonna leave that for the admin. And then I'll just hit save and stay. Alright. So now I'll be able to see sites up here. And again, you can see the problem is that I could go in and I could change the title of this to Site B Sucks. Not cool. So how do we actually lock this down from being edited? Right? Directus gives us a great way to do that with custom permissions. And this is a little complicated the first time that you encounter it, but once you understand what's happening, it is incredibly powerful. So here we are on our access policy for content editors. Right? Anybody with this policy attached to their user directly or to their user role will have edit access for any of these things that we've set up. And these are just the individual collections. Right. And one way that Directus protects your data when you add a new collection, we don't give access rights to any of the those new collections by default. We like to keep things secure. But, the AgencyOS folks should not be able to see content for Site B. So what do we need to do? Right? We need to establish a relationship between site and the user. So to do that, we're gonna go back to our sites, and in this case we're gonna use that many to many relationship that we already talked about. We're gonna create a new field inside the sites collection called users. The related collection will be direct us users. We don't want to allow duplicates. We're gonna show a link. Great. We'll just go ahead and create this. Actually, I'm gonna delete that back up. Forgot to explain what's actually going on. In that many to many relationship, basically it creates a junction table for us. So let's try again. We'll do sites, our users, direct us users is the related collection. And instead of hitting save here, I'm gonna open up this advanced mode, and now you can see what's going on underneath the hood. Right? There's a junction collection that's being created inside our database. I'm gonna call this siteusers. By default this will get auto populated. You can go in and change it. Right. So I'm gonna clean up this convention just a little bit. So we've got the junction table will be siteusers, the site ID will be stored under site, the directus user ID will be stored as user, and then one important piece here is to add this corresponding field to the directus users collection as well, because we're gonna need to use that in our permissions. So we'll add a sort field, and the relational triggers here just let us set up, actions based on what happens to this specific thing, right. If we deselect the site users or we delete the sites, we delete one of the directest users, we probably wanna keep this thing clean and delete the record inside the junction table. Great. Alright. So what did we actually achieve here? So if I go into sites, we go to AgencyOS, I can now add that AgencyOS content editor to that specific site. Okay. Great, Bryant. Why can he still edit all of the content in site b? That's where our access policies come into play. Right? We're running on twenty minutes here. We still got plenty of time to set this up. What we're going to do now is restrict access to all of our content. So we've got all the relationships set up. We're ready to lock this down. What we're gonna do, we're gonna go into our sites. Right? The first thing that we wanna lock down if you're not part of site b, you shouldn't be seeing site b. Right? So we'll go into our read access. This has full read access. We're gonna use custom permissions to scope this down a bit. So we're gonna drill into our users, and this is our junction table, right? So the user, users dot users equals dollar sign underscore current user. So basically what we're saying is if the user is part of this site and they are then able to read information from that site. So why is that not showing up? Oh, we probably didn't set up permissions for the junction table. There we go. Okay. We don't want anybody to be able to adjust that. Okay. So now I can see all the users within a certain site for only sites that I am a part of. Right? So I can no longer see or search or find Site B for this example user that's logged in. Right? The AgencyOS Content Editor, they cannot see Site B. Now if I go into site b and I give them access to site b and adding them as a user of site b, refresh over here, boom, site b shows up. I remove them. You can now not get access to site b. Right? One problem though, while I don't have access to the actual site, I can still see the pages for site b. So I here's where a bit of the tedious part comes in, where I just go in and actually edit our permissions for all of these different items. Right? So we wanna be able to edit, create new pages, but when it comes to reading and updating pages, we want to make sure that those are scoped properly. So the site, we're gonna go back to that junction table. Right? Site users, user equals current user. So because we have a relationship to the site from pages, we are now able to check all the users within the site and make sure that the ID for those includes the current user ID. And what I'm gonna do here, I'm gonna just copy this rule, and I'm gonna go through and paste this rule into the item's permissions item permissions. And I could go through and do that for other things as well, like our post collection. Where are you posts? Right? Copy paste this rule. Again, we'll add the rule there. We don't want them to be able to delete pages that don't belong to their site. Same thing for posts. And, you know, depending on your scenario, right, whether you're setting up multi site for different clients or different teams within a larger organization or, yeah, some combination thereof, you've got total control of this by creating different access policies, different roles. So the access policies I love, because I can get really granular about what what content I want to make available to read, update, create, etcetera. Alright. So for the sake of time, I'm not gonna go through and adjust all of these things, but if I just hit save and stay, now I should not be able to edit any content for site b or even see it unless I'm part of that site. And that applies for the API as well, right. So if I go in and I do items slash pages, we make this pretty print, I don't see any of those pages. Right? And just to prove this to you, we've got a page here that belongs to site b. If I copy the ID of this, and remember I'm logged in as the content editor for site, AgencyOS over here, right, I'm gonna get a error that I do not have permission to access this specific page. So great. That works really nicely, Bryant. Cool. How do we actually implement this on a, like, front end scenario? Right? How should we set this up? And one pattern that I follow quite a bit is creating a bot user for my front end. Right? So now that we've got this data model set up, everything is flowing permission wise the way that we want it, right we would go through and create our front ends. So you could have your front ends all in Next. Js, Nuxt, whatever front end framework you prefer. You can even have different front end frameworks. You're gonna wanna fetch that content and either generate your front end statically, dynamically, server side rendered, incremental static regeneration, all the different ways of rendering your content, you know, all available to you via the API. But one pattern that I like, I will go in. I'm just gonna create a new role. I'm gonna call it bot. And this is just a role for our website front ends to access the API. Alright, so within roles we are going to add content policies. Right? So we don't want our front end to actually be able to edit the content, we just want it to read content. Potentially we might want to let it upload files or submit forms. But for now, let's just keep this simple. We are going to let it read content. And one of the other roles that comes with Directus out of the box is the public role. So the public role controls what API data is available without authentication. Now usually I will take advantage of the public role, and you can see we have different policies here. So I can let the general public read all the published content. That's okay. In this multi tenant scenario, you might want to lock that down. Right? So I'll just remove all access for the public role. And if I get really crazy with this, go to Safari, we go to items, we go to pages, right, now I don't get access to any of the content. Right? I cannot have access to pages. But, if I am still logged in over here, we can get access to our pages. Right. Right. And, again, we're only seeing the page data for agent c o s. Cool. So we've created this bot user or this bot role. Let's take a look at our read policies. We'll just go ahead and fix all of these other collections that we would need to render on the front end. Probably the navigation. Great sites. Cool. And again, you can see some of these are already, kind of partial. 50% opacity here just highlighted where it says partial read access. Let's take a look at, like, pages for instance. I'm gonna save this really quickly though. So let's take a look at pages. Right? The rule for pages is that it has to be published, and the published date has to be now or before now. Makes sense, right? But we wanna add one more stipulation to this that any of these bot users can only access the sites that they have access to. So again, we can use that same setup where we go into the Junction collection, we use the current user. And with these bot users, I would highly recommend just creating one bot per website. So if you've got 50 websites, you create 50 bot users, that will be able to give you a token that will only, be able to access data for that specific site. And that way you don't have to keep passing something like site ID equals x y z in the parameters or every single call. Because that can get tedious, honestly. So here we're gonna adjust posts to do the same thing. We want the site, we want the underscore users user equal current user. And again, this is just a dynamic variable. Directus will populate that user ID for you there. Did we do that for pages as well? Let's do it for pages. Where are you? Sites. Tabs, users. Current user. Great. And if I want to, like, copy and paste these entire rules, these are just the standard filter rule syntax. If you check out docs.directus.io, you can find all of these things. Right. Great. Okay. So now we've got that locked down. Let's go in and create a new bot. Right? Let's say bot, site, site b bot. Crack myself up sometimes. And here, like, basically, we don't need an email and password. These folks aren't gonna log in to the data studio. Right? This is just gonna be a user for our front end to access the data. So in this case, we're gonna add that bot role. Cool. So I can know how many of these bots that we have. One other thing to note is that I don't even have to create a separate role. I could just give them read access, through the policies. So you've got a lot of flexibility there, and, you know, maybe you've got that one content editor who needs to do a little bit more than the others. Instead of having to create a separate role for them, just give them the policy that they need. What I'm gonna do here is create a static token. This just allows us to access it and we're gonna add this bot to Site B. Alright. Cool. Great. Save. Now we've got this user. And, cool. So now, what are we gonna do? I want to just demonstrate this. I don't think we're gonna have enough time. We've got like ten minutes before the hour's up. We're probably not gonna be able to dive into anything front end for that'll be tremendously helpful. Right? But let's just pretend like we're doing this on the front end. Documents. Great. We'll create a multi site collection here. I'm just using this HTTP client called Bruno. Usually, we do not talk about Bruno, but in this case, we are talking about Bruno. So I will grab the URL here from our API, and I'm gonna pretend to be the site b front end here. So we're gonna try to fetch our pages for site b. If I just make this full screen, I should get an error. Right? We do not have permission to access this. Rightfully so, we disabled all public access. But now if I go in and I add my token, right, now without having to add a param for site or or any of this mess. Right? I am only getting the content for site b because of the relationships and the permissions that I set up. So this bot user is a part of site b, and we can fix that to actually display this. So we'll go into direct us users. We'll go to sites. We want to display the site name. We'll display the site name in our display templates. Great. So now if we go back to bot for site b, alright, we can see the sites that they're available to. Now, if you're creating a lot of sites, you know, in this process of creating bots for each site are kind of cumbersome. Anytime you create a new site you could potentially run a flow that creates this bot user automatically, etcetera, so that you don't have to mess with this. It just depends on how many different sites that you're managing and what that setup kinda looks like. But here you can already see, let's go in and, you know, if I add the bot to the AgencyOS site, I save that. And if I rerun this API call, you can see now I'm getting all of those pages for the AgencyOS site as well, which is not what we want in this case. But, that way we can lock down the data that each one of these folks have access to. And this could be an unlimited number of front ends. So I hope that is it for this one. That is creating a multi site CMS. Right? Just to recap, we've got a sites collection that we created. We shifted all of our settings for the site into the actual sites collection themselves. Then we have relationships for pages, posts, etcetera, and we set up a bot user to access that for our front end. Right? On the front end, we would then just set up an API call to the direct us API. Not gonna get into that. That is gonna be opening a can of worms with five minutes left, and I don't wanna disappoint myself. So with that, multisite CMS, we're calling that a wrap. That's a win for this episode of 100 apps, one hundred hours. I hope you'll tune in to the next one, and make sure you check out our documentation for more insight on how to set some of these things up. I'll see you.","68ed164f-6d7d-4aba-adab-50be7e5b7b09",[576],"febb5a0f-decf-49bc-a161-899e94abf23c",[],{"id":142,"number":143,"show":122,"year":144,"episodes":579},[146,147,148,149,150,151,152,153,154,155],{"id":148,"slug":581,"vimeo_id":582,"description":583,"tile":584,"length":585,"resources":8,"people":8,"episode_number":143,"published":556,"title":586,"video_transcript_html":587,"video_transcript_text":588,"content":8,"seo":589,"status":130,"episode_people":590,"recommendations":592,"season":593},"rabbitar-directory","1059434943","Hop along with Bryant as he attempts to build a management system for AI-generated rabbit avatars (\"Rabbitars\"). Watch him tackle importing hundreds of images, setting up trait relationships, and creating an automated flow to generate new characters with DALL-E—all in just 60 minutes.","0a355b7b-d425-4db4-b7c2-4ad9fd964a6e",58,"Mission: Rabbitar Directory","\u003Cp>Speaker 0: Alright. Alright. Alright. Welcome back to yet another episode of 100 apps, one hundred hours. I'm your host Brian Gillespie, and this is the show where we try to build or rebuild some of your favorite apps or even crazy off the wall app ideas in one hour or less.\u003C/p>\u003Cp>Today, we've got a fun episode for you, an AI Rabatar directory, and I'll explain what that means in just a moment. But, the rules, if you're new to the show, you have sixty minutes to plan and build an application, no more, no less, and we're gonna use whatever tools we have at our disposal, which for me lately has, been a bit of, like, Cursor AI, the, AI enabled IDE. Been using it. I like it a lot for auto completion and things like that. Also, we've got some pieces for this one from a previous project.\u003C/p>\u003Cp>So the AI Ravitar directory. Let's take a look at at what we've got. Right? So this is a concept that we had in our Leap Week registration system. So for Leap Week three, back in the summertime, you could go in and when you registered, you could generate an AI Ravatar, that filled your astronaut suit.\u003C/p>\u003Cp>This was a space theme. You pick your traits, and it would generate a custom Ravatar for you that we then use in, like, the OG images on the the actual ticket pages. So lots of cool stuff there. But, what we're gonna do today is take this and transform it into a directory of these Ravatars with their traits that we could potentially reuse somewhere else. So with that, let's start the clock and dig into this thing.\u003C/p>\u003Cp>What is the actual functionality that we want out of this? And, you know, this is usually where I like to start anytime I'm building an application. We're just gonna do this. And did we yep. Okay.\u003C/p>\u003Cp>Alright. So the functionality that we want out of this. Right? We want a list of well, basically, manage a lot of rabbitars. So we wanna be able to, you know, edit those, generate them.\u003C/p>\u003Cp>Let's say generate new rabbitars based on a prompt, and, you know, manage traits. Manage a lot of rabbitars. You know? Do we get potentially, like, serve random rabbitars? I don't know.\u003C/p>\u003Cp>Serve random Ravitar based on a seed. Alright. So what is our data model for this thing looks like? Hey. This is one that I'm kind of building on the fly here.\u003C/p>\u003Cp>I haven't really thought much about this. You know, some of the other ones, I think a a little more about before we pop in here, but, obviously, we're gonna have our rabbitars as the top level. If we drill down into that, what does that actually look like? We've got, a relationship to traits that we'll have. We probably got a title for the Ravitar.\u003C/p>\u003Cp>We probably have a description. If I remember this right, maybe we call it, like, prompt because that's what we had from our Leap Week setup. And then we've got a list of traits, and we can make those relational. So we'll have that. We'll just connect the dots there.\u003C/p>\u003Cp>That'll be a mini to mini relationship. Right? Mini to mini relationship between traits and Ravatars, and this will just have a trait. And, you know, maybe we can add a description to that if we want to. No big deal.\u003C/p>\u003Cp>No big deal either way. This looks pretty good. You know, if you watched the show before, I've I've got, like, a Nuxt app starter that I use from time to time. I've downloaded a list of the files and prompts from our Leap Week system and, you know, anonymize this data so that it is secure. And then we've just created a blank project on Directus cloud.\u003C/p>\u003Cp>You can see there's a totally blank instance. So let's dive right in. Right? The first thing we're gonna do, let's create our Ravatar collection. Right?\u003C/p>\u003Cp>So we're gonna call this Ravatars. For the primary key, let's use a generated UUID for this, and we'll have a created at. We can say created by, or we could just use the naming convention that we have here. This is one of my favorite features inside Directus. Like, you can adjust these, but, basically, this will give you, you know, the standard time stamps, the standard relationship to the user that created these things.\u003C/p>\u003Cp>We don't really need a status for these. Right? Once they're created, they're created. We don't need a sort. So let's just add that.\u003C/p>\u003Cp>There we go. We've got our avatars and now we're gonna create these extra fields like title, prompt, and then we'll go through and do the traits. Right? So let's actually change this to a name. We'll give each one of these Ravatars a name somehow.\u003C/p>\u003Cp>Maybe we could use AI to do that since this is an AI generated episode. Great. Cool. Well, now we've got a name. Pretty simple.\u003C/p>\u003Cp>Just a string input. What else do we have? We have a prompt. You know, what was the actual prompt that we used to generate this avatar? That could be handy information.\u003C/p>\u003Cp>What else? And then we need the actual avatar. So we need the file. Forgot to do that one, didn't I? So let's do the file.\u003C/p>\u003Cp>I could use file or image here. It's the same kind of relationship. It just depends on what type it is. So if it's an image, which we are doing images, I would choose the image interface here because it will show, like, a nice thumbnail preview. So we're just gonna call this the file for the avatar or ravitar, and we can add padding to this if we need to.\u003C/p>\u003Cp>Great. Now let's work on traits. Right? So traits is gonna be a separate collection for me just because I want to be able to add new traits easily and, you know, be able to query the number of rabbitars based on a trait. And that gets a little trickier to do if you're using, you know, traits here as a a field in, like, CSV format.\u003C/p>\u003Cp>So I'm just gonna create a new table. We're gonna call it traits. And instead of the primary key field being ID with, like, an auto incremented integer here, I'm just gonna do this. Let's call the primary key field trait, and we're gonna use a manually entered string. That'll be nice.\u003C/p>\u003Cp>You know, created at, created by, updated at. I really don't need this, but we'll add it anyway. Great. And now we've got a trait field or trait collection, I should say. And, you know, I could go through and add a description to this if I wanted to, you know, just extra info, really.\u003C/p>\u003Cp>Alright. And and I could even go as far as, like, using a label. So where I'm using this as the primary key, you know, I might want to, like, slugify that or make sure that it everything is, like, in camel case or something like that. But cool. So you see how easy it is to add these extra fields to direct us.\u003C/p>\u003Cp>Let's do, cruelty free. Yeah. That's my favorite icon. I don't know why this is called cruelty free, but that gets us the bunny that we're looking for. Awesome.\u003C/p>\u003Cp>And then we'll add traits. I don't know if there's an icon for this or not. Traits attributes. Looks good enough. Attributes.\u003C/p>\u003Cp>Where are you? There we go. Alright. Just a little little touch of OCD there. Cool.\u003C/p>\u003Cp>So now we've got avatars. We can see we got a name. We got a prompt. We got a file. We got traits.\u003C/p>\u003Cp>But these two are not linked together. Right? So, what we want to do next is just link these two together. That is going to be using a many to many relationship inside Directus and I'm going to call this field traits. So, you can see I'm in the Ravatars collection.\u003C/p>\u003Cp>I'm going to go in and add a new field called traits and the related collection is going to be traits. And what I'm going to do here, we'll show a link to that, but for now what I'm going to do is continue in advanced field creation mode. And we're gonna go to the relationship. So here, we can see that Directus is gonna create a junction table for us. Let's call it ravitar traits, and I'm gonna change the fields inside that collection.\u003C/p>\u003Cp>So this will be called ravatar. This will be called trait. And we can add that corresponding field to the traits collection as well. That way I can look at any of the ravatars that have a trait. And I guess we can add a sort field here.\u003C/p>\u003Cp>So the relational triggers, if I bring up my little mouse pointer tool here, it's called mouse posay, by the way. If you are interested in this little thing that you've you've caught me use a couple times, Mouse Pose if you're on a Mac. Pretty neat little piece of software for creating videos or doing training. Alright. Back to the lecture at hand.\u003C/p>\u003Cp>So we will go in and these relational triggers are just whenever these certain events happen. And, basically, if I deselect a trait on a Ravatar or I delete a trait, what happens? And more times than not, when I'm using many to many relationships, I set these up so that, it will delete the junction table item because I I still have the data in each individual collection. Cool. So with that, let's go ahead and create this mini to mini relationship.\u003C/p>\u003Cp>And now I can see the traits here. I've got traits there. And you can see inside the data model that junction table that was created, ravitar traits. So, you know, if I open this up and I create a a new ravitar, right, we can add existing traits, add new traits, blah blah blah blah blah blah blah blah blah. Alright.\u003C/p>\u003Cp>So now what I've got working with here is this should be set up correctly for this kind of data model. Right? And this is ripped out of our Leap Week project that I was talking about that I won't showcase here, but you can get access to something similar inside our Directus plus subscription. It's called the event registration kit, and it comes with a a NUCs front end with the, like, the detailed badges and things like that. But we got the file.\u003C/p>\u003Cp>We've got the prompt. We've got the individual traits that should have the correct keys to a trait. I wanna say that will create the trait, but we'll take a look once we actually figure that out or not. And this may throw an error when we try to import it. But for now, like, what I'm concerned with is this file.\u003C/p>\u003Cp>Right? If I look inside this project, I don't have any files. So what I've done in another directory here, I've used our template CLI tool to just export all the files and assets from that project. So here we go. I've got all the actual rabbitars here.\u003C/p>\u003Cp>We could see what all those fine fellows look like and and ladies. Let's try to use the Directus template CLI tool to get those into this project. So we got rabbitars.directus.app. We're gonna go in to my users here. I'm gonna create a token for this.\u003C/p>\u003Cp>We'll save that token. And what do I wanna do? We're gonna go to c d desktop /leapweek. Do we have something there? We got templates.\u003C/p>\u003Cp>Okay. Good. Let's c d into templates. We got the Leap Week template. And now we're gonna do, let's actually just pull this thing up so we can see what's going on.\u003C/p>\u003Cp>Template CLI. Open that up in a new tab. Got this going on. Alright. So the latest version of this does have, like, a programmatic mode where I can apply partials.\u003C/p>\u003Cp>So I'm gonna just copy this into my editor. We're gonna go h t t p s, what is this? Ravatars.directus.app. I'm gonna paste my token. Reminder to self roll this token before we do this.\u003C/p>\u003Cp>My template is leap week. Template type equals local. I'm gonna do a partial, and we're gonna do files. So that flag should get us, oh, and I think partial is, like, the p part of it as well. Is that what that stands for?\u003C/p>\u003Cp>P is a partial? Partial apply p. Okay. So we wanna do files only. Fingers crossed if this is actually gonna work or not.\u003C/p>\u003Cp>That's the beauty of this sort of thing. Alright. We hit go. It's logged in as the admin user. This doesn't look like it's doing what we want.\u003C/p>\u003Cp>Row row, Raggy. Maybe loading the files, and then we may be having to do some cleanup work after the fact. So I probably lied about a flag here or something. Well, let's just open it up and see. It's always fun.\u003C/p>\u003Cp>What else has this done for us? We'll just make this full screen again. Rabbitars. And it is uploading the files here, so we're probably getting a bit of that. And you could see now we do have events.\u003C/p>\u003Cp>We we've got this entire Leap Week template, which, is grand. So we'll we'll let this process these files, I guess, before we try to do anything else. This is gonna eat up some time on the clock, I'm afraid. I don't know how we're going here. One of the the nice things that we have recently added in Directus is the ability to see the logs.\u003C/p>\u003Cp>So these are coming in real time, and I could see, all the logs that are coming in. And you could see that we've got files here. So it should be uploading these files. And if I open this tab, you know, we should start to see some of those files uploading. But this is just a handy way to check on what's happening inside the direct us instance in real time.\u003C/p>\u003Cp>Now the template CLI tool that I've got here, it has a rate limit of, like, 10 requests a second in here. So that's why you're seeing, like, these things being staggered, which is helpful to not kind of overload. And these are not small files either. So this is a lot of bandwidth that I'm eating up here. But now you can start to see some of the the actual avatars.\u003C/p>\u003Cp>We just hope that the import of the JSON goes well once this is done. Now, maybe we try to work on some other piece of this while this is loading. I can't imagine how long this is gonna take. There's 924. We're doing 10 a second times 924 divided by 10.\u003C/p>\u003Cp>That gives us a 92 divided by 60. Minute and a half. It's taking a little longer than that, isn't it? Anyway, let's dive in and I don't think it's going to mess up if we start cleaning this up, but it's a risky run. Right?\u003C/p>\u003Cp>I'm gonna start deleting these collections that I did not actually need in this case. And then I'm gonna go back to the drawing board at some point after this and figure out why that partial apply did not work. Still loading files, people, ravatar. We don't need any of these blocks. We're just gonna clean this up.\u003C/p>\u003Cp>And again, what Directus is doing underneath the hood here, it has basically added these tables to my SQL database. But once I, you know, delete these or add new collections or any changes that I make through Directus through the studio, the underlying Directus APIs are making those changes to the SQL database in real time, which is nice. Right? So if I were to take a look now, like, all these tables would be in my SQL database, but we're just gonna clean these up. Who said this is not fun?\u003C/p>\u003Cp>Make sure we're not removing the fields that we actually need for this. Still loading files. Still loading files. Alright. Delete.\u003C/p>\u003Cp>Delete. Delete. And this is how you know that these things aren't scripted because if it was, I would be way more organized than this. Alright. So now we've got our collections back where we need it.\u003C/p>\u003Cp>Are we we're still getting all the files in here? Files. Files. Files. Files.\u003C/p>\u003Cp>Files. Just posting away. Anyway, let's try to figure out what we're gonna do next. Right? We've got the av Ravatars in there.\u003C/p>\u003Cp>We're gonna wanna generate new Ravatars at some point. How can we do that? Right? For that, let's go into the marketplace. And I believe we have an AI image generation option.\u003C/p>\u003Cp>Yes. Or operation here. So this is part of the Directus AI pack that we have. We've got the AI image generation option which calls DALL E three to generate an image. Right?\u003C/p>\u003Cp>So let's install this guy. We'll refresh. That will pull up that. Are we done with the files? No.\u003C/p>\u003Cp>We are not. There's another one that I'm gonna pull in here, the AI writer operation. So this is basically just a shortcut for the different AI LLM providers like OpenAI, Anthropic, Meta, Llama, all those different ones. And you can use a couple different options here. So we're just gonna install this too in case we need that.\u003C/p>\u003Cp>And let's talk about, like, creating new Revatars. We'll refresh. Still plugging away on the images. Man, how many rabbitars worth this in in in total? Let's let's just see.\u003C/p>\u003Cp>924 Ravatars. We're awaiting more logs. Are we done? Post. Post.\u003C/p>\u003Cp>I don't see oh, there we go again. Coming back. We're running more images. But, yeah. You can actually see the scalability here.\u003C/p>\u003Cp>Like, we're just hammering this thing with images and everything else is running a okay. Alright. So let's tackle this Gravatar generation piece. Right? If I want to create a new Ravitar, right, we're probably going to pass it some traits.\u003C/p>\u003Cp>And maybe we give it a name. Right? So let's organize this just a little bit because the two things that I want are gonna be up here. Like, I'm not gonna give this a prompt each time. I just want to store the prompt that was used to generate that.\u003C/p>\u003Cp>And we may even have something like this, like a metadata field. I I use this pattern a lot as well where I just store some JSON data that comes back from these. Alright. So basically, we'll give it a name, we'll add a few traits, and then we will store the rest of this info. And we could even go in and add, like, a group to this, like a detail group.\u003C/p>\u003Cp>Yeah. We'll call this generation, AI generation. Great. Do we have, like, a little robot icon? Perfect.\u003C/p>\u003Cp>Cool. So now we can just stuff all these in here, Get a little bit of organization. And we might want to even like update a status or something like that. Right? I didn't think we needed a status field here, but maybe we do wanna have a status for, like, generating or generation.\u003C/p>\u003Cp>Maybe we won't mess with that at this moment. Alright. Okay. So it looks like it's got all the files. That's all we need.\u003C/p>\u003Cp>I'm just gonna cancel that. Do we have 924 files? Is that what it shows? Let's see. All files.\u003C/p>\u003Cp>Refresh. We're done sending files through here. Boom. Boom. Boom.\u003C/p>\u003Cp>Boom. Oh, have I somehow logged myself out? No. Okay. I don't know what happened there.\u003C/p>\u003Cp>Alright. So we do have 924 files. Great. We'll take a detour again from this. But now we've got, like, this nice AI generation thing that, you know, we could see, like, the actual file.\u003C/p>\u003Cp>Maybe we wanna pull a file back out of this. K. I'm sorry. I'm a bit scattered here. I haven't had enough coffee yet this morning.\u003C/p>\u003Cp>Alright. So now I've got this JSON file that I exported from, the the directory, the Leap Week. Let's go in and actually try to import this file. We'll go to Directus, hundred apps, Nuxt starter. I've got output dot JSON.\u003C/p>\u003Cp>Let's open this up and see if we can get this imported. It says invalid foreign key. That's not good. Should we run this type of script again? Trait.\u003C/p>\u003Cp>Let's just go in and create a rabbitar. Let's create this first one. File. Trait. Where's our file?\u003C/p>\u003Cp>Oh, I'm inside traits. Did I import inside the trait directory? Maybe that could be the problem. We'll just edit the raw value. Let's add the file there.\u003C/p>\u003Cp>That should find the file. We don't have any of these traits, so we'll create one. Sunglasses. Masculine. Serious.\u003C/p>\u003Cp>Great. Beard and mustache. And if I add this prompt there we go. Alright. So I'm just going to delete this guy from our file since we're adding that now.\u003C/p>\u003Cp>We'll hit save and stay. Now we've got our our actual file here. We've got the list of prompts or traits. Serious rabbit face. Mcrabbit.\u003C/p>\u003Cp>Serious rabbit face Mcrabbit. There's our directory of, this guy. We can see we've got the traits now, and we're showing, like, the actual rabbitars that we have here. And there's the list of traits. Great.\u003C/p>\u003Cp>Cool. Makes sense. And then we could even clean that up a bit if we wanted to, but let's now let me try to import this again just to see. I think I may have had the wrong directory. Invalid foreign key.\u003C/p>\u003Cp>Alright. Let's see. Let's try this again. We'll hit start import. What are we getting back from the for the trait collection, Ravitar traits?\u003C/p>\u003Cp>Invalid foreign key in Ravitar traits. It's always always fun. Alright. So what I'm gonna do, I'm gonna just open up this inside a new tab. I'm gonna wanna wanna see the data we're getting back from the API.\u003C/p>\u003Cp>Right? So here I could see this is the actual data. Let's do something like this where we add an asterisk, we get fields, we get traits, and I just wanna get all the root level fields for traits. There's the trait. I guess, do we have to go through and get all the individual traits?\u003C/p>\u003Cp>Is that the problem? It should be this should be the right syntax. Alright. I got this script. Where is this guy?\u003C/p>\u003Cp>So we're transforming the snake case. Trait. Trait. We shouldn't have to do that, should we? Alright.\u003C/p>\u003Cp>Let's think about this a little bit further. Do we need to stuff this into a create option, or is it just the fact that we need to get the traits? Alright. This is where things like cursor come in handy. Alright.\u003C/p>\u003Cp>Write another script that will find all the unique traits and output into a JSON array. Extract unique traits, blah blah blah. Let's just call this traits dot j s. And then you can even go as far as, like, how what command to run this. No transform.\u003C/p>\u003Cp>Blah blah blah. Maybe that didn't help much, but node traits dot j s. Was that output dot JSON or no? This is, this is gonna pull the oh, that's gonna grab my actual other one. What is this thing?\u003C/p>\u003Cp>Transform. There we go. Traits dot j s, and then we'll do this as what? Oh, traits. Where are we at here?\u003C/p>\u003Cp>I'm in the wrong directory. Output traits. Traits dot j s. FS is not defined. Man.\u003C/p>\u003Cp>Just having fun with this all day here. Alright. Two snake cases not defined, of course. Should've just copied this entire file over. Alright.\u003C/p>\u003Cp>So if we got the traits alright. So we'll do this the old fashioned way. Sunglasses, serious, smiling, great, short hair, eyeglasses, Bald. Black hair. Hat.\u003C/p>\u003Cp>Blonde hair. How fast can you type, Bryant? There we go. Alright. How are we doing on time?\u003C/p>\u003Cp>We got, like, thirty minutes left. We got feminine, lipstick. What else we got? Necklace. Neutral.\u003C/p>\u003Cp>Red hair. Gray hair. And because this is the primary key, if I try to do one like sunglasses that we already have, it should throw this out. Yeah. Value has to be unique.\u003C/p>\u003Cp>Okay. So I I think we've got them all. Did we get smiling? I don't know if we got that one or not. Smiling.\u003C/p>\u003Cp>We did get smiling. Okay. Alright. Let's try to import this again now that we have all those existing traits. If not, like, we could just use the create, add new syntax, invalid foreign key.\u003C/p>\u003Cp>Yep. Alright. Let's go back to our script that I wrote here just to get this data correct. Traits, we are going to add this in a we're gonna wrap this in this syntax. So create, update, delete.\u003C/p>\u003Cp>And then we're gonna pull this. So this will basically give us this syntax, if I pull it up in the docs. Right? Traits. Not traits.\u003C/p>\u003Cp>Create, update, delete. Is that gonna show? Basically, whenever you're creating items in here if we make this full screen, Relational data. So Directus allows you to create relationships or relational data through a specific collection. Sometimes it's better to use the detailed changes syntax.\u003C/p>\u003Cp>So, basically, you say, okay. Here's the records we wanna create. Here's the ones we want to update. Here's the ones we want to delete. And that should populate everything that we need.\u003C/p>\u003Cp>We're just gonna run this one again. Node transform. Output JSON. Alright. So this should create a record in the junction collection.\u003C/p>\u003Cp>Let's try it again. Output dot JSON. Start import. Invalid foreign key. Don't tell me that, sir.\u003C/p>\u003Cp>Do not tell me that. We need those traits. Start import. In the collection, Ravitar traits. Invalid foreign key for the field trait.\u003C/p>\u003Cp>That is the foreign key. Did I goof that up? Ravitar traits. Trait, Ravitar. Create trait sunglasses.\u003C/p>\u003Cp>Let's just shorten this up. We'll try it with a different file. Yeah. Can we get one of these to actually import? Import one dot JSON.\u003C/p>\u003Cp>Still not sure what I'm doing wrong, but that's the beauty of doing this live. For trait value is required for the trait. There is a trait. Import one dot JSON. File prompt.\u003C/p>\u003Cp>Oh, I'm inside the trait. Start import. Import one. Okay. So we got one to import.\u003C/p>\u003Cp>I don't understand what we're doing here. Why isn't the full file working? Eyeglasses, brown hair. It should be working. Right.\u003C/p>\u003Cp>Now we can see that works. Was I just in the wrong one again? Not paying attention? No. Is there one where trait is empty?\u003C/p>\u003Cp>No. Feminine red. Where's our output traits again? Traits. Alright.\u003C/p>\u003Cp>Let's just do this. We'll do a little sort. Sort lines ascending. Bald beard, black hair, blonde hair. They're missing brown hair.\u003C/p>\u003Cp>Yeah. So if I was smart, I would have, figured out the way to make these traits actually auto populate. But, again, that's the beauty of doing these things live. Import one. Okay.\u003C/p>\u003Cp>That works. So we'll just delete these guys out. Cool. Let's try the actual output file. We're still missing one, aren't we?\u003C/p>\u003Cp>Bald, brown, black, earrings, eyeglasses, feminine, gray hair, hat, lipstick, masculine, long hair. Long hair. Attention to detail matters, Brian. Long hair, masculine mustache, necklace, neutral, red hair, serious, short hair, smiling sunglasses. Okay.\u003C/p>\u003Cp>Now we have them all. We've just seriously burnt a lot of time on this, and that is gonna put us in a time crunch. But no stress. Right? So this should now take all of that data, import our avatars, make sure the files are linked up, and this may take a moment just because it is, like, digesting all those relationships as well.\u003C/p>\u003Cp>What do we got left? Twenty three minutes. Where are we at? We've got a lot of rabbitars that we can manage. We can now manage our traits as well, which is nice.\u003C/p>\u003Cp>How do we generate new rabbitars? Right? Cool. If I wanted to just update these a bit, I could go in and show a tiny image preview here just so we could see what that rabbitar actually looks like. And then I would probably go in here and update my interface to show, like, the trait name as well.\u003C/p>\u003Cp>And we'll display related values. We'll show the trait name. So now I can see, like, the actual traits here. Smiling brown here. I don't have a name for those.\u003C/p>\u003Cp>We probably not gonna get to that portion of it. I guess we could use AI to generate a list of names for these, and you could probably even pass it. Like, some of the ones like OpenAI, you could pass it the the actual image of this and have it give it a name. But here, I don't even really need the prompt. Right?\u003C/p>\u003Cp>I just wanna see the individual traits. Cool. Neutral brown hair. There's that. And I can make this a little more cozy, or what I might wanna do is just add this to a card's layout.\u003C/p>\u003Cp>Traits. Trait. We're under pressure trying to generate too many files, maybe. Bump bump bump. There we go.\u003C/p>\u003Cp>Oh, that's a good one there. Yeah. So now I get to see these actual Ravatars, and you'd be able to navigate these through the file library as well. It's just nice to be able to see them through the lens of the the associated traits as well. Right?\u003C/p>\u003Cp>So if I went here and I went to trait, it should be showing those. I don't know why it's not. But anyway, there we go. We want to be able to generate new rabbitars as well. Let's go through and create that step.\u003C/p>\u003Cp>Right? So anytime a new rabbitar is generated, generate new rabbitars. Generate new rabbitar. Let's do this. Is there a new icon?\u003C/p>\u003Cp>Perfect. You know, I love icons. Alright. So anytime we create a new Ravitar entry, after that's created, let's go in and call that OpenAPI a OpenAI API. I always get hung up on that.\u003C/p>\u003Cp>So here's our collection. So anytime an item is created in the Ravitar collection, we're going to run this flow. And I could test that just by doing something like this. We go into Ravitar and create a new one. We're going to call this Doctor.\u003C/p>\u003Cp>Phil the Ravitar. What traits is he going to have? Bald, gray hair, eyeglasses, Sirius. Cool. There's our traits.\u003C/p>\u003Cp>Alright, we hit Save and Stay. Cool. And there's Doctor. Phil, right? So that should have actually triggered this flow to run.\u003C/p>\u003Cp>And we can see that in the logs and it should pass us the actual data. So we can see here, here's the individual traits. Great. Cool. Now what?\u003C/p>\u003Cp>Now we are going to generate an image. Alright. Generate image, and that is gonna be using the AI image generation. And now we're gonna need an open API key, platform.openai.com. Log in.\u003C/p>\u003Cp>How are we doing on time? We got 19 left, man. We're cooking now. Let's get logged in. Alright.\u003C/p>\u003Cp>Where are you? Dashboard. API keys. We're gonna create a new secret key. Direct to sleep week.\u003C/p>\u003Cp>Okay. Yeah. We'll do that. This is what we call rabbitars. Create the secret key.\u003C/p>\u003Cp>Copy. Let's throw this in here. That's the secret. Alright. Do I have these are part of the rules.\u003C/p>\u003Cp>Right? Do I have the prompt that we use to actually generate these bad boys? That would be handy. That would save us a bit of time. Wouldn't it, Brian?\u003C/p>\u003Cp>Where are you? Leap week. Leap week. Leap week. Leap week.\u003C/p>\u003Cp>What is this? Leap week? Leap week. No. That's the actual template, isn't it?\u003C/p>\u003Cp>Oh, boy. Alright. Directus. Where are you? Lots of projects in here.\u003C/p>\u003Cp>Event OS. Virtual events, I think, is what this is. Server API. Is is this the one? Let's see.\u003C/p>\u003Cp>There we go. Okay. So there's my prompt. Photorealistic head on headshot of a single rabbit set against a back black background with detailed fur and realistic texture, and then we're gonna pass it the attributes that we want. Let's give it a name, traits.\u003C/p>\u003Cp>And we'll do standard quality square as the size. Alright. So now what I'm gonna do here is just add a little connector, and we could see, like, the data that we're getting back. I just wanna clean this up. We'll just copy this over here, and we'll make this half screen.\u003C/p>\u003Cp>So I'm gonna do a little run script operation. Cleanup is what we'll call it. And we're gonna take the data coming from this. So we'll do here's the, let's just call it the payload. And that will be under the data key, we'll find trigger because it's coming from the trigger.\u003C/p>\u003Cp>That will be the payload, and then we're gonna return traits. Right? So we want const traits equals payload dot create dot map, trait. And we want to return trait dot trait. Return trait.\u003C/p>\u003Cp>We just wanna return that, don't we? Trait dot trait. Traits. Okay. The name will be payload dot name, if it exists.\u003C/p>\u003Cp>And no. Actually, that one should exist. Payload dot name. Name. Payload dot name.\u003C/p>\u003Cp>I need a comma and traits. And I could paste this in here just to verify that this is actual test dot j s. Is this actually properly formatted, or are we we're throwing an error there. What am I missing? Oh, we're missing a there we go.\u003C/p>\u003Cp>No. Return. Okay. Concentrates. I think that should be proper now, except we don't need that.\u003C/p>\u003Cp>We got the return in the wrong place. Okeydoke. I think this is gonna work. And, you know, always test first. So we'll just go in and create another avatar.\u003C/p>\u003Cp>Be Jeezy. He is going to have, what, long hair. How we doing on time? Fifteen minutes left. Super masculine sunglasses.\u003C/p>\u003Cp>Save. What do we get coming back? Are we seeing this the way we want it to be? Cannot read payload dot map. Payload dot create.\u003C/p>\u003Cp>What was the actual payload? Traits dot create. Payload. Data dot trigger dot payload. That doesn't need to be asynchronous.\u003C/p>\u003Cp>Data dot trigger dot payload data dot trigger. Was there not a payload? I could've swore there was. Yeah. Trigger dot payload.\u003C/p>\u003Cp>Turn trait. That'll loop through these. We should have a trait dot trait. Create dot map. Create traits out.\u003C/p>\u003Cp>Yep. There you go. Sometimes you miss a option there. Let's try it again. BG.\u003C/p>\u003Cp>Sunglasses. Masculine. Long hair. I just have this up in a bun right now, by the way. Jiraiya new Ravitar.\u003C/p>\u003Cp>Cool. Now we get the traits. Can we just return the actual list of trait? Treat dot treat dot treat dot treat. Okay.\u003C/p>\u003Cp>Cool. Flows. We have new flows. There we go. Okay.\u003C/p>\u003Cp>So this is what we're gonna pass to open AI API. Cool. And traits is gonna be cleanup dot traits. And the name, cleanup dot traits. Cool.\u003C/p>\u003Cp>And then we will pass cleanup dot name. K. And then the next thing that we're gonna wanna do, we should get something back from this, but then we'll have to import a file as well. But let's test this out. We could try that doctor Phil one again.\u003C/p>\u003Cp>That's great. Beard, gray hair, bald, serious. Woah. Save. And I could even open this up in a new tab if we wanted to do some fanciness here.\u003C/p>\u003Cp>Just flows. Alright. We see this flow less than a minute ago. Okay. This has given us this is the actual URL.\u003C/p>\u003Cp>Okay. So there's the Rabatar that was generated. There it is. Square standard. Here's the URL.\u003C/p>\u003Cp>Right? So now we need to import this URL into Directus. Right? So for that, let's go to the docs. And let's look at the API reference.\u003C/p>\u003Cp>So we're going to go to files accessing files. We don't want to access we want to create files. Importing a file. Alright, so using the REST API we can import a file. And what we're going to do here is basically just call this thing.\u003C/p>\u003Cp>Import file. Cool. We will use the webhook request. This is what? A post request to ravatars dot directus dot app slash files slash import.\u003C/p>\u003Cp>And we're gonna pass it two things. Right? We're gonna pass it a URL. URL is what? What's the URL?\u003C/p>\u003Cp>What did I call that last step? Generate image. So we're gonna access that data through this key, and I I think it'll actually just be generate image because it looks like all we're getting back from them is a string. And then we're gonna pass an authorization header here. Or what I could do instead is just allow anybody the ability to upload a file here, which if we save that, I can go into our access policies, go to the public one, and allow anybody to create a file.\u003C/p>\u003Cp>That's what I'm gonna do here. This is not recommended for production use. Obviously, you don't want anybody in the general public with the ability to upload files. Generate a new rabbitar. Cool.\u003C/p>\u003Cp>And this should the last step of this one is going to be do we get the actual ID of the item? Right? There's our key. So that's gonna be trigger dot key. What we're gonna do is update this.\u003C/p>\u003Cp>We're gonna update that rabbitar. Update rabbitar. From the collection there. We're going to do full access just to make sure we've got it. Then we're going to do this.\u003C/p>\u003Cp>We'll do the trigger. Key. And then the payload, we're gonna have the file that we get back from that last operation. So file is gonna be import oh my gosh. Is it just called import?\u003C/p>\u003Cp>It's just called import. Alright. So we'll do file. So that's the inside the Ravitar collection, that's where we're storing the relationship to the file. And that will be import dot data dot id.\u003C/p>\u003Cp>I wanna say that's what it should be. Import.data.id. If it's not, it's not. Okeydoke. Alright.\u003C/p>\u003Cp>So let's try this again. We're gonna do Bry Guy, Bry Ross. Cool. Bry Ross has red hair, smiling, eyeglasses, long hair, very masculine, and a beard. K.\u003C/p>\u003Cp>We'll hit save and stay. And I think I had I had Directus pulling it up at at one point here, didn't I? Let's just go in. We'll check our flows. Less than a minute ago.\u003C/p>\u003Cp>Update Ravitar. Invalid syntax for type UUID. Okay. So we got this back. Data.data.ID is what we need.\u003C/p>\u003Cp>So we didn't quite get the updated one. Data dot data dot ID. Alright. But I should be able to see that Ravitar in there. Right?\u003C/p>\u003Cp>Yeah. There we go. Alright. So let's try this one more time with six minutes left on the clock. What do we have here?\u003C/p>\u003Cp>Test. Bry Ross. We'll just pick a bunch of these eyeglasses. Red hair. Cool.\u003C/p>\u003Cp>Beard. Save and stay. And I'm gonna copy this link. We'll go over here. No.\u003C/p>\u003Cp>Not that one. Got arced again. Test. Bry. There it is.\u003C/p>\u003Cp>Right? So we can see here we created this fourteen seconds later. There we go. We have our AI Rabatar that is generated for this one. Bada bing badda boom.\u003C/p>\u003Cp>We generated new rabbitars. Now we can generate those on the fly, which is great. And because this is a direct us flow, even if I created these via the API, like passing traits, it would call that OpenAI API for me. Do we have enough time? We got five minutes.\u003C/p>\u003Cp>How can we generate a random avatar based on or how can we return a random avatar based on a seed? I I'm not even gonna attempt this one, to be honest with you, because, normally, I would wanna set this up on, like, a probably, like, a custom extension, instead of, like, a flow or something like that. But it it is doable. I wanna end on a high note. So just recapping.\u003C/p>\u003Cp>Right? We have generated a directory full of AI Ravatars that we can then manage and query and do all sorts of things with. We can manage traits, and we can see all the Ravatars that are within those as well. You know, we might even clean this up a little bit. So I can see for the interface, I wanna show the Ravitar and I wanna show the file for that Ravitar.\u003C/p>\u003Cp>So now if I take a look at these, right, we should be seeing, like, a little list of those Ravatars there. That's a particularly ugly one there. I'm not sure what happened with this one, but, there we go. That is generating an AI Gravatar directory. More goodness to come in the next episodes of 100 apps, one hundred hours.\u003C/p>\u003Cp>Join me. We'll see you. You.\u003C/p>","Alright. Alright. Alright. Welcome back to yet another episode of 100 apps, one hundred hours. I'm your host Brian Gillespie, and this is the show where we try to build or rebuild some of your favorite apps or even crazy off the wall app ideas in one hour or less. Today, we've got a fun episode for you, an AI Rabatar directory, and I'll explain what that means in just a moment. But, the rules, if you're new to the show, you have sixty minutes to plan and build an application, no more, no less, and we're gonna use whatever tools we have at our disposal, which for me lately has, been a bit of, like, Cursor AI, the, AI enabled IDE. Been using it. I like it a lot for auto completion and things like that. Also, we've got some pieces for this one from a previous project. So the AI Ravitar directory. Let's take a look at at what we've got. Right? So this is a concept that we had in our Leap Week registration system. So for Leap Week three, back in the summertime, you could go in and when you registered, you could generate an AI Ravatar, that filled your astronaut suit. This was a space theme. You pick your traits, and it would generate a custom Ravatar for you that we then use in, like, the OG images on the the actual ticket pages. So lots of cool stuff there. But, what we're gonna do today is take this and transform it into a directory of these Ravatars with their traits that we could potentially reuse somewhere else. So with that, let's start the clock and dig into this thing. What is the actual functionality that we want out of this? And, you know, this is usually where I like to start anytime I'm building an application. We're just gonna do this. And did we yep. Okay. Alright. So the functionality that we want out of this. Right? We want a list of well, basically, manage a lot of rabbitars. So we wanna be able to, you know, edit those, generate them. Let's say generate new rabbitars based on a prompt, and, you know, manage traits. Manage a lot of rabbitars. You know? Do we get potentially, like, serve random rabbitars? I don't know. Serve random Ravitar based on a seed. Alright. So what is our data model for this thing looks like? Hey. This is one that I'm kind of building on the fly here. I haven't really thought much about this. You know, some of the other ones, I think a a little more about before we pop in here, but, obviously, we're gonna have our rabbitars as the top level. If we drill down into that, what does that actually look like? We've got, a relationship to traits that we'll have. We probably got a title for the Ravitar. We probably have a description. If I remember this right, maybe we call it, like, prompt because that's what we had from our Leap Week setup. And then we've got a list of traits, and we can make those relational. So we'll have that. We'll just connect the dots there. That'll be a mini to mini relationship. Right? Mini to mini relationship between traits and Ravatars, and this will just have a trait. And, you know, maybe we can add a description to that if we want to. No big deal. No big deal either way. This looks pretty good. You know, if you watched the show before, I've I've got, like, a Nuxt app starter that I use from time to time. I've downloaded a list of the files and prompts from our Leap Week system and, you know, anonymize this data so that it is secure. And then we've just created a blank project on Directus cloud. You can see there's a totally blank instance. So let's dive right in. Right? The first thing we're gonna do, let's create our Ravatar collection. Right? So we're gonna call this Ravatars. For the primary key, let's use a generated UUID for this, and we'll have a created at. We can say created by, or we could just use the naming convention that we have here. This is one of my favorite features inside Directus. Like, you can adjust these, but, basically, this will give you, you know, the standard time stamps, the standard relationship to the user that created these things. We don't really need a status for these. Right? Once they're created, they're created. We don't need a sort. So let's just add that. There we go. We've got our avatars and now we're gonna create these extra fields like title, prompt, and then we'll go through and do the traits. Right? So let's actually change this to a name. We'll give each one of these Ravatars a name somehow. Maybe we could use AI to do that since this is an AI generated episode. Great. Cool. Well, now we've got a name. Pretty simple. Just a string input. What else do we have? We have a prompt. You know, what was the actual prompt that we used to generate this avatar? That could be handy information. What else? And then we need the actual avatar. So we need the file. Forgot to do that one, didn't I? So let's do the file. I could use file or image here. It's the same kind of relationship. It just depends on what type it is. So if it's an image, which we are doing images, I would choose the image interface here because it will show, like, a nice thumbnail preview. So we're just gonna call this the file for the avatar or ravitar, and we can add padding to this if we need to. Great. Now let's work on traits. Right? So traits is gonna be a separate collection for me just because I want to be able to add new traits easily and, you know, be able to query the number of rabbitars based on a trait. And that gets a little trickier to do if you're using, you know, traits here as a a field in, like, CSV format. So I'm just gonna create a new table. We're gonna call it traits. And instead of the primary key field being ID with, like, an auto incremented integer here, I'm just gonna do this. Let's call the primary key field trait, and we're gonna use a manually entered string. That'll be nice. You know, created at, created by, updated at. I really don't need this, but we'll add it anyway. Great. And now we've got a trait field or trait collection, I should say. And, you know, I could go through and add a description to this if I wanted to, you know, just extra info, really. Alright. And and I could even go as far as, like, using a label. So where I'm using this as the primary key, you know, I might want to, like, slugify that or make sure that it everything is, like, in camel case or something like that. But cool. So you see how easy it is to add these extra fields to direct us. Let's do, cruelty free. Yeah. That's my favorite icon. I don't know why this is called cruelty free, but that gets us the bunny that we're looking for. Awesome. And then we'll add traits. I don't know if there's an icon for this or not. Traits attributes. Looks good enough. Attributes. Where are you? There we go. Alright. Just a little little touch of OCD there. Cool. So now we've got avatars. We can see we got a name. We got a prompt. We got a file. We got traits. But these two are not linked together. Right? So, what we want to do next is just link these two together. That is going to be using a many to many relationship inside Directus and I'm going to call this field traits. So, you can see I'm in the Ravatars collection. I'm going to go in and add a new field called traits and the related collection is going to be traits. And what I'm going to do here, we'll show a link to that, but for now what I'm going to do is continue in advanced field creation mode. And we're gonna go to the relationship. So here, we can see that Directus is gonna create a junction table for us. Let's call it ravitar traits, and I'm gonna change the fields inside that collection. So this will be called ravatar. This will be called trait. And we can add that corresponding field to the traits collection as well. That way I can look at any of the ravatars that have a trait. And I guess we can add a sort field here. So the relational triggers, if I bring up my little mouse pointer tool here, it's called mouse posay, by the way. If you are interested in this little thing that you've you've caught me use a couple times, Mouse Pose if you're on a Mac. Pretty neat little piece of software for creating videos or doing training. Alright. Back to the lecture at hand. So we will go in and these relational triggers are just whenever these certain events happen. And, basically, if I deselect a trait on a Ravatar or I delete a trait, what happens? And more times than not, when I'm using many to many relationships, I set these up so that, it will delete the junction table item because I I still have the data in each individual collection. Cool. So with that, let's go ahead and create this mini to mini relationship. And now I can see the traits here. I've got traits there. And you can see inside the data model that junction table that was created, ravitar traits. So, you know, if I open this up and I create a a new ravitar, right, we can add existing traits, add new traits, blah blah blah blah blah blah blah blah blah. Alright. So now what I've got working with here is this should be set up correctly for this kind of data model. Right? And this is ripped out of our Leap Week project that I was talking about that I won't showcase here, but you can get access to something similar inside our Directus plus subscription. It's called the event registration kit, and it comes with a a NUCs front end with the, like, the detailed badges and things like that. But we got the file. We've got the prompt. We've got the individual traits that should have the correct keys to a trait. I wanna say that will create the trait, but we'll take a look once we actually figure that out or not. And this may throw an error when we try to import it. But for now, like, what I'm concerned with is this file. Right? If I look inside this project, I don't have any files. So what I've done in another directory here, I've used our template CLI tool to just export all the files and assets from that project. So here we go. I've got all the actual rabbitars here. We could see what all those fine fellows look like and and ladies. Let's try to use the Directus template CLI tool to get those into this project. So we got rabbitars.directus.app. We're gonna go in to my users here. I'm gonna create a token for this. We'll save that token. And what do I wanna do? We're gonna go to c d desktop /leapweek. Do we have something there? We got templates. Okay. Good. Let's c d into templates. We got the Leap Week template. And now we're gonna do, let's actually just pull this thing up so we can see what's going on. Template CLI. Open that up in a new tab. Got this going on. Alright. So the latest version of this does have, like, a programmatic mode where I can apply partials. So I'm gonna just copy this into my editor. We're gonna go h t t p s, what is this? Ravatars.directus.app. I'm gonna paste my token. Reminder to self roll this token before we do this. My template is leap week. Template type equals local. I'm gonna do a partial, and we're gonna do files. So that flag should get us, oh, and I think partial is, like, the p part of it as well. Is that what that stands for? P is a partial? Partial apply p. Okay. So we wanna do files only. Fingers crossed if this is actually gonna work or not. That's the beauty of this sort of thing. Alright. We hit go. It's logged in as the admin user. This doesn't look like it's doing what we want. Row row, Raggy. Maybe loading the files, and then we may be having to do some cleanup work after the fact. So I probably lied about a flag here or something. Well, let's just open it up and see. It's always fun. What else has this done for us? We'll just make this full screen again. Rabbitars. And it is uploading the files here, so we're probably getting a bit of that. And you could see now we do have events. We we've got this entire Leap Week template, which, is grand. So we'll we'll let this process these files, I guess, before we try to do anything else. This is gonna eat up some time on the clock, I'm afraid. I don't know how we're going here. One of the the nice things that we have recently added in Directus is the ability to see the logs. So these are coming in real time, and I could see, all the logs that are coming in. And you could see that we've got files here. So it should be uploading these files. And if I open this tab, you know, we should start to see some of those files uploading. But this is just a handy way to check on what's happening inside the direct us instance in real time. Now the template CLI tool that I've got here, it has a rate limit of, like, 10 requests a second in here. So that's why you're seeing, like, these things being staggered, which is helpful to not kind of overload. And these are not small files either. So this is a lot of bandwidth that I'm eating up here. But now you can start to see some of the the actual avatars. We just hope that the import of the JSON goes well once this is done. Now, maybe we try to work on some other piece of this while this is loading. I can't imagine how long this is gonna take. There's 924. We're doing 10 a second times 924 divided by 10. That gives us a 92 divided by 60. Minute and a half. It's taking a little longer than that, isn't it? Anyway, let's dive in and I don't think it's going to mess up if we start cleaning this up, but it's a risky run. Right? I'm gonna start deleting these collections that I did not actually need in this case. And then I'm gonna go back to the drawing board at some point after this and figure out why that partial apply did not work. Still loading files, people, ravatar. We don't need any of these blocks. We're just gonna clean this up. And again, what Directus is doing underneath the hood here, it has basically added these tables to my SQL database. But once I, you know, delete these or add new collections or any changes that I make through Directus through the studio, the underlying Directus APIs are making those changes to the SQL database in real time, which is nice. Right? So if I were to take a look now, like, all these tables would be in my SQL database, but we're just gonna clean these up. Who said this is not fun? Make sure we're not removing the fields that we actually need for this. Still loading files. Still loading files. Alright. Delete. Delete. Delete. And this is how you know that these things aren't scripted because if it was, I would be way more organized than this. Alright. So now we've got our collections back where we need it. Are we we're still getting all the files in here? Files. Files. Files. Files. Files. Just posting away. Anyway, let's try to figure out what we're gonna do next. Right? We've got the av Ravatars in there. We're gonna wanna generate new Ravatars at some point. How can we do that? Right? For that, let's go into the marketplace. And I believe we have an AI image generation option. Yes. Or operation here. So this is part of the Directus AI pack that we have. We've got the AI image generation option which calls DALL E three to generate an image. Right? So let's install this guy. We'll refresh. That will pull up that. Are we done with the files? No. We are not. There's another one that I'm gonna pull in here, the AI writer operation. So this is basically just a shortcut for the different AI LLM providers like OpenAI, Anthropic, Meta, Llama, all those different ones. And you can use a couple different options here. So we're just gonna install this too in case we need that. And let's talk about, like, creating new Revatars. We'll refresh. Still plugging away on the images. Man, how many rabbitars worth this in in in total? Let's let's just see. 924 Ravatars. We're awaiting more logs. Are we done? Post. Post. I don't see oh, there we go again. Coming back. We're running more images. But, yeah. You can actually see the scalability here. Like, we're just hammering this thing with images and everything else is running a okay. Alright. So let's tackle this Gravatar generation piece. Right? If I want to create a new Ravitar, right, we're probably going to pass it some traits. And maybe we give it a name. Right? So let's organize this just a little bit because the two things that I want are gonna be up here. Like, I'm not gonna give this a prompt each time. I just want to store the prompt that was used to generate that. And we may even have something like this, like a metadata field. I I use this pattern a lot as well where I just store some JSON data that comes back from these. Alright. So basically, we'll give it a name, we'll add a few traits, and then we will store the rest of this info. And we could even go in and add, like, a group to this, like a detail group. Yeah. We'll call this generation, AI generation. Great. Do we have, like, a little robot icon? Perfect. Cool. So now we can just stuff all these in here, Get a little bit of organization. And we might want to even like update a status or something like that. Right? I didn't think we needed a status field here, but maybe we do wanna have a status for, like, generating or generation. Maybe we won't mess with that at this moment. Alright. Okay. So it looks like it's got all the files. That's all we need. I'm just gonna cancel that. Do we have 924 files? Is that what it shows? Let's see. All files. Refresh. We're done sending files through here. Boom. Boom. Boom. Boom. Oh, have I somehow logged myself out? No. Okay. I don't know what happened there. Alright. So we do have 924 files. Great. We'll take a detour again from this. But now we've got, like, this nice AI generation thing that, you know, we could see, like, the actual file. Maybe we wanna pull a file back out of this. K. I'm sorry. I'm a bit scattered here. I haven't had enough coffee yet this morning. Alright. So now I've got this JSON file that I exported from, the the directory, the Leap Week. Let's go in and actually try to import this file. We'll go to Directus, hundred apps, Nuxt starter. I've got output dot JSON. Let's open this up and see if we can get this imported. It says invalid foreign key. That's not good. Should we run this type of script again? Trait. Let's just go in and create a rabbitar. Let's create this first one. File. Trait. Where's our file? Oh, I'm inside traits. Did I import inside the trait directory? Maybe that could be the problem. We'll just edit the raw value. Let's add the file there. That should find the file. We don't have any of these traits, so we'll create one. Sunglasses. Masculine. Serious. Great. Beard and mustache. And if I add this prompt there we go. Alright. So I'm just going to delete this guy from our file since we're adding that now. We'll hit save and stay. Now we've got our our actual file here. We've got the list of prompts or traits. Serious rabbit face. Mcrabbit. Serious rabbit face Mcrabbit. There's our directory of, this guy. We can see we've got the traits now, and we're showing, like, the actual rabbitars that we have here. And there's the list of traits. Great. Cool. Makes sense. And then we could even clean that up a bit if we wanted to, but let's now let me try to import this again just to see. I think I may have had the wrong directory. Invalid foreign key. Alright. Let's see. Let's try this again. We'll hit start import. What are we getting back from the for the trait collection, Ravitar traits? Invalid foreign key in Ravitar traits. It's always always fun. Alright. So what I'm gonna do, I'm gonna just open up this inside a new tab. I'm gonna wanna wanna see the data we're getting back from the API. Right? So here I could see this is the actual data. Let's do something like this where we add an asterisk, we get fields, we get traits, and I just wanna get all the root level fields for traits. There's the trait. I guess, do we have to go through and get all the individual traits? Is that the problem? It should be this should be the right syntax. Alright. I got this script. Where is this guy? So we're transforming the snake case. Trait. Trait. We shouldn't have to do that, should we? Alright. Let's think about this a little bit further. Do we need to stuff this into a create option, or is it just the fact that we need to get the traits? Alright. This is where things like cursor come in handy. Alright. Write another script that will find all the unique traits and output into a JSON array. Extract unique traits, blah blah blah. Let's just call this traits dot j s. And then you can even go as far as, like, how what command to run this. No transform. Blah blah blah. Maybe that didn't help much, but node traits dot j s. Was that output dot JSON or no? This is, this is gonna pull the oh, that's gonna grab my actual other one. What is this thing? Transform. There we go. Traits dot j s, and then we'll do this as what? Oh, traits. Where are we at here? I'm in the wrong directory. Output traits. Traits dot j s. FS is not defined. Man. Just having fun with this all day here. Alright. Two snake cases not defined, of course. Should've just copied this entire file over. Alright. So if we got the traits alright. So we'll do this the old fashioned way. Sunglasses, serious, smiling, great, short hair, eyeglasses, Bald. Black hair. Hat. Blonde hair. How fast can you type, Bryant? There we go. Alright. How are we doing on time? We got, like, thirty minutes left. We got feminine, lipstick. What else we got? Necklace. Neutral. Red hair. Gray hair. And because this is the primary key, if I try to do one like sunglasses that we already have, it should throw this out. Yeah. Value has to be unique. Okay. So I I think we've got them all. Did we get smiling? I don't know if we got that one or not. Smiling. We did get smiling. Okay. Alright. Let's try to import this again now that we have all those existing traits. If not, like, we could just use the create, add new syntax, invalid foreign key. Yep. Alright. Let's go back to our script that I wrote here just to get this data correct. Traits, we are going to add this in a we're gonna wrap this in this syntax. So create, update, delete. And then we're gonna pull this. So this will basically give us this syntax, if I pull it up in the docs. Right? Traits. Not traits. Create, update, delete. Is that gonna show? Basically, whenever you're creating items in here if we make this full screen, Relational data. So Directus allows you to create relationships or relational data through a specific collection. Sometimes it's better to use the detailed changes syntax. So, basically, you say, okay. Here's the records we wanna create. Here's the ones we want to update. Here's the ones we want to delete. And that should populate everything that we need. We're just gonna run this one again. Node transform. Output JSON. Alright. So this should create a record in the junction collection. Let's try it again. Output dot JSON. Start import. Invalid foreign key. Don't tell me that, sir. Do not tell me that. We need those traits. Start import. In the collection, Ravitar traits. Invalid foreign key for the field trait. That is the foreign key. Did I goof that up? Ravitar traits. Trait, Ravitar. Create trait sunglasses. Let's just shorten this up. We'll try it with a different file. Yeah. Can we get one of these to actually import? Import one dot JSON. Still not sure what I'm doing wrong, but that's the beauty of doing this live. For trait value is required for the trait. There is a trait. Import one dot JSON. File prompt. Oh, I'm inside the trait. Start import. Import one. Okay. So we got one to import. I don't understand what we're doing here. Why isn't the full file working? Eyeglasses, brown hair. It should be working. Right. Now we can see that works. Was I just in the wrong one again? Not paying attention? No. Is there one where trait is empty? No. Feminine red. Where's our output traits again? Traits. Alright. Let's just do this. We'll do a little sort. Sort lines ascending. Bald beard, black hair, blonde hair. They're missing brown hair. Yeah. So if I was smart, I would have, figured out the way to make these traits actually auto populate. But, again, that's the beauty of doing these things live. Import one. Okay. That works. So we'll just delete these guys out. Cool. Let's try the actual output file. We're still missing one, aren't we? Bald, brown, black, earrings, eyeglasses, feminine, gray hair, hat, lipstick, masculine, long hair. Long hair. Attention to detail matters, Brian. Long hair, masculine mustache, necklace, neutral, red hair, serious, short hair, smiling sunglasses. Okay. Now we have them all. We've just seriously burnt a lot of time on this, and that is gonna put us in a time crunch. But no stress. Right? So this should now take all of that data, import our avatars, make sure the files are linked up, and this may take a moment just because it is, like, digesting all those relationships as well. What do we got left? Twenty three minutes. Where are we at? We've got a lot of rabbitars that we can manage. We can now manage our traits as well, which is nice. How do we generate new rabbitars? Right? Cool. If I wanted to just update these a bit, I could go in and show a tiny image preview here just so we could see what that rabbitar actually looks like. And then I would probably go in here and update my interface to show, like, the trait name as well. And we'll display related values. We'll show the trait name. So now I can see, like, the actual traits here. Smiling brown here. I don't have a name for those. We probably not gonna get to that portion of it. I guess we could use AI to generate a list of names for these, and you could probably even pass it. Like, some of the ones like OpenAI, you could pass it the the actual image of this and have it give it a name. But here, I don't even really need the prompt. Right? I just wanna see the individual traits. Cool. Neutral brown hair. There's that. And I can make this a little more cozy, or what I might wanna do is just add this to a card's layout. Traits. Trait. We're under pressure trying to generate too many files, maybe. Bump bump bump. There we go. Oh, that's a good one there. Yeah. So now I get to see these actual Ravatars, and you'd be able to navigate these through the file library as well. It's just nice to be able to see them through the lens of the the associated traits as well. Right? So if I went here and I went to trait, it should be showing those. I don't know why it's not. But anyway, there we go. We want to be able to generate new rabbitars as well. Let's go through and create that step. Right? So anytime a new rabbitar is generated, generate new rabbitars. Generate new rabbitar. Let's do this. Is there a new icon? Perfect. You know, I love icons. Alright. So anytime we create a new Ravitar entry, after that's created, let's go in and call that OpenAPI a OpenAI API. I always get hung up on that. So here's our collection. So anytime an item is created in the Ravitar collection, we're going to run this flow. And I could test that just by doing something like this. We go into Ravitar and create a new one. We're going to call this Doctor. Phil the Ravitar. What traits is he going to have? Bald, gray hair, eyeglasses, Sirius. Cool. There's our traits. Alright, we hit Save and Stay. Cool. And there's Doctor. Phil, right? So that should have actually triggered this flow to run. And we can see that in the logs and it should pass us the actual data. So we can see here, here's the individual traits. Great. Cool. Now what? Now we are going to generate an image. Alright. Generate image, and that is gonna be using the AI image generation. And now we're gonna need an open API key, platform.openai.com. Log in. How are we doing on time? We got 19 left, man. We're cooking now. Let's get logged in. Alright. Where are you? Dashboard. API keys. We're gonna create a new secret key. Direct to sleep week. Okay. Yeah. We'll do that. This is what we call rabbitars. Create the secret key. Copy. Let's throw this in here. That's the secret. Alright. Do I have these are part of the rules. Right? Do I have the prompt that we use to actually generate these bad boys? That would be handy. That would save us a bit of time. Wouldn't it, Brian? Where are you? Leap week. Leap week. Leap week. Leap week. What is this? Leap week? Leap week. No. That's the actual template, isn't it? Oh, boy. Alright. Directus. Where are you? Lots of projects in here. Event OS. Virtual events, I think, is what this is. Server API. Is is this the one? Let's see. There we go. Okay. So there's my prompt. Photorealistic head on headshot of a single rabbit set against a back black background with detailed fur and realistic texture, and then we're gonna pass it the attributes that we want. Let's give it a name, traits. And we'll do standard quality square as the size. Alright. So now what I'm gonna do here is just add a little connector, and we could see, like, the data that we're getting back. I just wanna clean this up. We'll just copy this over here, and we'll make this half screen. So I'm gonna do a little run script operation. Cleanup is what we'll call it. And we're gonna take the data coming from this. So we'll do here's the, let's just call it the payload. And that will be under the data key, we'll find trigger because it's coming from the trigger. That will be the payload, and then we're gonna return traits. Right? So we want const traits equals payload dot create dot map, trait. And we want to return trait dot trait. Return trait. We just wanna return that, don't we? Trait dot trait. Traits. Okay. The name will be payload dot name, if it exists. And no. Actually, that one should exist. Payload dot name. Name. Payload dot name. I need a comma and traits. And I could paste this in here just to verify that this is actual test dot j s. Is this actually properly formatted, or are we we're throwing an error there. What am I missing? Oh, we're missing a there we go. No. Return. Okay. Concentrates. I think that should be proper now, except we don't need that. We got the return in the wrong place. Okeydoke. I think this is gonna work. And, you know, always test first. So we'll just go in and create another avatar. Be Jeezy. He is going to have, what, long hair. How we doing on time? Fifteen minutes left. Super masculine sunglasses. Save. What do we get coming back? Are we seeing this the way we want it to be? Cannot read payload dot map. Payload dot create. What was the actual payload? Traits dot create. Payload. Data dot trigger dot payload. That doesn't need to be asynchronous. Data dot trigger dot payload data dot trigger. Was there not a payload? I could've swore there was. Yeah. Trigger dot payload. Turn trait. That'll loop through these. We should have a trait dot trait. Create dot map. Create traits out. Yep. There you go. Sometimes you miss a option there. Let's try it again. BG. Sunglasses. Masculine. Long hair. I just have this up in a bun right now, by the way. Jiraiya new Ravitar. Cool. Now we get the traits. Can we just return the actual list of trait? Treat dot treat dot treat dot treat. Okay. Cool. Flows. We have new flows. There we go. Okay. So this is what we're gonna pass to open AI API. Cool. And traits is gonna be cleanup dot traits. And the name, cleanup dot traits. Cool. And then we will pass cleanup dot name. K. And then the next thing that we're gonna wanna do, we should get something back from this, but then we'll have to import a file as well. But let's test this out. We could try that doctor Phil one again. That's great. Beard, gray hair, bald, serious. Woah. Save. And I could even open this up in a new tab if we wanted to do some fanciness here. Just flows. Alright. We see this flow less than a minute ago. Okay. This has given us this is the actual URL. Okay. So there's the Rabatar that was generated. There it is. Square standard. Here's the URL. Right? So now we need to import this URL into Directus. Right? So for that, let's go to the docs. And let's look at the API reference. So we're going to go to files accessing files. We don't want to access we want to create files. Importing a file. Alright, so using the REST API we can import a file. And what we're going to do here is basically just call this thing. Import file. Cool. We will use the webhook request. This is what? A post request to ravatars dot directus dot app slash files slash import. And we're gonna pass it two things. Right? We're gonna pass it a URL. URL is what? What's the URL? What did I call that last step? Generate image. So we're gonna access that data through this key, and I I think it'll actually just be generate image because it looks like all we're getting back from them is a string. And then we're gonna pass an authorization header here. Or what I could do instead is just allow anybody the ability to upload a file here, which if we save that, I can go into our access policies, go to the public one, and allow anybody to create a file. That's what I'm gonna do here. This is not recommended for production use. Obviously, you don't want anybody in the general public with the ability to upload files. Generate a new rabbitar. Cool. And this should the last step of this one is going to be do we get the actual ID of the item? Right? There's our key. So that's gonna be trigger dot key. What we're gonna do is update this. We're gonna update that rabbitar. Update rabbitar. From the collection there. We're going to do full access just to make sure we've got it. Then we're going to do this. We'll do the trigger. Key. And then the payload, we're gonna have the file that we get back from that last operation. So file is gonna be import oh my gosh. Is it just called import? It's just called import. Alright. So we'll do file. So that's the inside the Ravitar collection, that's where we're storing the relationship to the file. And that will be import dot data dot id. I wanna say that's what it should be. Import.data.id. If it's not, it's not. Okeydoke. Alright. So let's try this again. We're gonna do Bry Guy, Bry Ross. Cool. Bry Ross has red hair, smiling, eyeglasses, long hair, very masculine, and a beard. K. We'll hit save and stay. And I think I had I had Directus pulling it up at at one point here, didn't I? Let's just go in. We'll check our flows. Less than a minute ago. Update Ravitar. Invalid syntax for type UUID. Okay. So we got this back. Data.data.ID is what we need. So we didn't quite get the updated one. Data dot data dot ID. Alright. But I should be able to see that Ravitar in there. Right? Yeah. There we go. Alright. So let's try this one more time with six minutes left on the clock. What do we have here? Test. Bry Ross. We'll just pick a bunch of these eyeglasses. Red hair. Cool. Beard. Save and stay. And I'm gonna copy this link. We'll go over here. No. Not that one. Got arced again. Test. Bry. There it is. Right? So we can see here we created this fourteen seconds later. There we go. We have our AI Rabatar that is generated for this one. Bada bing badda boom. We generated new rabbitars. Now we can generate those on the fly, which is great. And because this is a direct us flow, even if I created these via the API, like passing traits, it would call that OpenAI API for me. Do we have enough time? We got five minutes. How can we generate a random avatar based on or how can we return a random avatar based on a seed? I I'm not even gonna attempt this one, to be honest with you, because, normally, I would wanna set this up on, like, a probably, like, a custom extension, instead of, like, a flow or something like that. But it it is doable. I wanna end on a high note. So just recapping. Right? We have generated a directory full of AI Ravatars that we can then manage and query and do all sorts of things with. We can manage traits, and we can see all the Ravatars that are within those as well. You know, we might even clean this up a little bit. So I can see for the interface, I wanna show the Ravitar and I wanna show the file for that Ravitar. So now if I take a look at these, right, we should be seeing, like, a little list of those Ravatars there. That's a particularly ugly one there. I'm not sure what happened with this one, but, there we go. That is generating an AI Gravatar directory. More goodness to come in the next episodes of 100 apps, one hundred hours. Join me. We'll see you. You.","2685fdc9-9197-4838-b93b-5e4610dcdddb",[591],"0c98dc96-64e5-43c7-81dd-d74f5e324ed9",[],{"id":142,"number":143,"show":122,"year":144,"episodes":594},[146,147,148,149,150,151,152,153,154,155],{"id":149,"slug":596,"vimeo_id":597,"description":598,"tile":599,"length":524,"resources":8,"people":8,"episode_number":135,"published":556,"title":600,"video_transcript_html":601,"video_transcript_text":602,"content":8,"seo":603,"status":130,"episode_people":604,"recommendations":606,"season":607},"help-center","1059435433","Bryant tackles the challenge of building a documentation site that lets content editors manage help articles through a CMS instead of markdown files. Watch as he sets up a backend with Directus—creating categories, articles, and tags relationships—before diving into a frontend template to bring it all together in his race against the clock.","866ba612-4d80-485a-ad0e-7f438ec602a9","Mission: Help Center","\u003Cp>Speaker 0: What's up peeps? Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie for Directus. Today, we've got a pretty cool episode, I think. Yeah.\u003C/p>\u003Cp>I've seen this come up time and time again in our community and some of the questions. You know, most documentation sites or help centers, at least in the developer community, they're all static markdown files, which is great. As a developer, I love markdown plain text formatting in a nice easy to read way, easy for me to author. Content editors, they don't necessarily love markdown. So today, we're going to be building a help center.\u003C/p>\u003Cp>If you're new to 100 apps, one hundred hours, there are two rules. We have sixty minutes to plan and build an application, an idea, a clone, whatever, no more, no less. And number two, use whatever we have at our disposal. So with that, let's fire sixty minutes on the clock and go. So I usually start every episode just trying to figure out what type of functionality we're looking for here.\u003C/p>\u003Cp>So I want a documentation site, a help center slash documentation site where let's do this on a new line, where content editors can edit within a CMS and not necessarily text files or markdown or whatever. Right? Those are the main requirements here. If we were to put down, like, a stretch goal on this, I would say, like, IT and IN support could be a we'll call it a stretch goal. Sounds good?\u003C/p>\u003Cp>Sounds great. Alright. So as far as what we want out of our data model here. Right? When I think of a help center, I think of categories.\u003C/p>\u003Cp>So I think of what section do these belong in, and I think of articles. So category has what? It has a name. It has articles within it. So there's a relationship there.\u003C/p>\u003Cp>The articles belong to a category. Could there be multiple categories or multiple could an article live in multiple categories? I would say not. I would say that's probably the realm for our friend tags. Right?\u003C/p>\u003Cp>Or topics or something like that. Right? I can have a mini to mini relationship. So we'll do some nice looking arrows. Great.\u003C/p>\u003Cp>Great looking arrows. So we have multiple tags on an article, but an article belongs to a single category. Right? So we have a title for the article. We have a slug and then we have content.\u003C/p>\u003Cp>Now this could be markdown if you prefer that. It could be HTML from a WYSIWYG editor. However we wanna do that, we'll tackle that as we dive in. Alright. So before we even do the front end, let's start on this inside Directus.\u003C/p>\u003Cp>Now I've got a nice kind of front end template picked out for this, but, let's start back end first with our data. Right? The first thing we're gonna create, let's let's tackle categories. Right? So we'll do categories.\u003C/p>\u003Cp>We could call these collections. We call a table inside direct us a collection, so that could be confusing. Let's just call it a category. Great. We'll give it a UUID and we'll hit save.\u003C/p>\u003Cp>Do I want any of these other things? We probably want sorts for these. You know, I could decide not to publish an entire category or not. I guess that's okay. Let's give this a name.\u003C/p>\u003Cp>Category name, makes sense. Article title, I am we could go with title across the board. I guess that makes more sense. Alright. So let's go ahead and start cleaning up the form a bit.\u003C/p>\u003Cp>I like title, status. Do we have a slug for the category? Probably. Forgot to put that in there. We can even add nice little icons in Directus.\u003C/p>\u003Cp>Makes it super simple. We'll just add a little link. I'm gonna open up the interface for this and just Slugify this. Make my Slugify option using my little mouse Pose tool here. We'll trim the start and end in case there are white spaces, and let's make this mono space font.\u003C/p>\u003Cp>Yeah. This is gonna look amazing. Right? We'll use mono space font for the display. That just controls how it looks in the different directus layouts.\u003C/p>\u003Cp>We have a title, a slug. We have a category. What else do we want? We wanna have an icon. Right?\u003C/p>\u003Cp>Cool. Icon for this category. Can't get rid of the designer OCD stuff that I still hang on to. Cool. That looks great for categories.\u003C/p>\u003Cp>Now let's set up our articles. It doesn't look great. Forgot an icon for the actual category itself and a display template. Great. Okay.\u003C/p>\u003Cp>So now we have categories. I could go in and create a new category. Let's call this API reference. Right? API.\u003C/p>\u003Cp>And do we have an API? Yeah. Your standard API icon. We'll just go ahead and set this to published. Great.\u003C/p>\u003Cp>We have a category. I could query this on the front end. Items slash categories. Boom. I've got a REST API.\u003C/p>\u003Cp>Magic happens. Sparkles fall from the sky, etcetera. Alright. So we've got categories. Let's add articles now.\u003C/p>\u003Cp>So again, I'm going to caution against using something like slug here as the primary key field because if you ever need to change that slug, you cannot change the primary key value for a record because of all the relationships that are attached to it. Could get very messy. Right? So, what we'll do here, let's add created on, created by. These system fields here or these optional system fields are helpful.\u003C/p>\u003Cp>Created by you can also rename them as you see me doing here. Updated at, created at, updated at, updated by that'll be the user. Cool. You can use them as is. You can leave these out.\u003C/p>\u003Cp>You can add them back later. It's just a shortcut for you. Alright. So let's pick the article icon. Is there an article icon?\u003C/p>\u003Cp>What do you got for me? Directus. That looks pretty good for an article. We'll command s to save and stay. And let's start fiddling around with this, right?\u003C/p>\u003Cp>Again, we'll use a title for the article. Cool. Gravy. One of the other shortcuts that you can use in Directus, if you have a field that you want from another collection per se inside your data model, I can just go to duplicate. So I've already got the slug set up how I want it.\u003C/p>\u003Cp>We'll just dupe that across to the articles collection. Alright. And categories will probably be above that. So here I can see my slug field. We'll just set that to half width.\u003C/p>\u003Cp>What else did we have here? We had content. Right? So how are we gonna set up content? Do we want WYSIWYG?\u003C/p>\u003Cp>Do we want Markdown? It could go either way. Let's lean Markdown first just to see what this looks like. Hit save. And basically behind the scenes, this is just creating a text column inside the database.\u003C/p>\u003Cp>So, you know, if I wanted to go back later and adjust like the actual interface, what the user interacts with, I can change that from markdown to WYSIWYG very easily. Or if you're like me, you might use even plug something like Tiptap in here through our directus marketplace. Great. K. Now we've got tags.\u003C/p>\u003Cp>Let's go in and create our tags. Great. Generate a UUID. What is the tag? Do we have a sort for the tags?\u003C/p>\u003Cp>We don't really have any of this tags. We'll use a hashtag. What's the tag? Name of the tag, title of the tag? We'll just use title.\u003C/p>\u003Cp>Again, naming things is always the hardest thing. I think there's like a receipt option that kinda looks like a tag. Does that bookmark maybe? Kinda looks like a tag. Kinda also looks a lot like a bookmark.\u003C/p>\u003Cp>This does not look like a tag. Oh, that's a Pentagon. Pin tag on. Yeah. Cool.\u003C/p>\u003Cp>Let's use that. Sounds great. Alright. So now we have a tag. I don't know that we need a a slug on this.\u003C/p>\u003Cp>You know, you might have, like, grouping for tags later. But that's kind of the basic of this data model. Right? Now let's let's add an article to this, and we can quickly see what we're working with. Accessing items.\u003C/p>\u003Cp>Items. Accessing items. So there's our slug. We can publish this guy. We can go in and add some markdown.\u003C/p>\u003Cp>Hey, yo. This is a markdown editor. It edits markdown. Wild, I know, right? And then I could preview that.\u003C/p>\u003Cp>We could save this. Again, we can just, like, look at this URL. We can replace items with the admin. So here we can see this data. This is because we're actually logged in.\u003C/p>\u003Cp>When we send this request, it is sending our token in the cookie. If I were to do this in, like, a incognito window, you'll see that I I get no access. No access to that. We could solve that by going in and updating our access policies and and or our roles. So, by default, Directus loads two roles.\u003C/p>\u003Cp>There's a public and an administrator role. We've got our access policies here which are, more granular. Right? I can have I can attach 35 different access policies to a single role if I want to. But if we look here, the data that is available without authentication is absolutely zero.\u003C/p>\u003Cp>Absolutely nothing because we want you to be secure. So we'll just go in, we'll add tags. We're probably gonna want files if we choose to upload them. So we'll add read access to files, add read access to tags, add read access to categories and articles. The one restriction I'm gonna put on articles is that we only wanna show published articles.\u003C/p>\u003Cp>Pretty straightforward. I guess we could do this to categories as well because we do we have a status? Yes. We only wanna show published categories. Now if I were to test that out, we just create a this is unpublished article.\u003C/p>\u003Cp>This is a work in progress. That's what we'll call it. This is WIP. Great. I got two articles now.\u003C/p>\u003Cp>If I open up that incognito window, paste that same URL except we drop the ID, you could see I'm only getting a single article that is published. If I were to change the WIP article to published, boom, boom, fetch. Yeah. There we go. I can now see that WIP article.\u003C/p>\u003Cp>Great. Permissions works. We have a REST API. We have what we need to proceed. So, the thing we are missing here is our relationships.\u003C/p>\u003Cp>Right? We don't have a relationship between articles and tags and categories and vice versa. So let's solve for that. Right? First thing, we need to define kinda the way those relationships work.\u003C/p>\u003Cp>There is a one to many relationship between categories and articles and a many to many relationship between our tags. So I could do this. I could go into our categories. We'll look for that one to many relationship. We'll call this articles.\u003C/p>\u003Cp>The related collection is also gonna be called articles. So the key is the name of the field inside the categories collection, the related collection here, and then we have a foreign key, which will be the category, probably a a nice naming convention here, singular, plural, fun stuff. So the category is only going to be a pointer back to the categories collection. And for the displayed template, we could show the title. Great.\u003C/p>\u003Cp>Maybe we wanted to do show if this is actually published or not. I think that's a good way to handle it. And we'll show a link to this item. And if I wanted to drill into this, I can go open the advanced field and we could see here's the related collection. You know, maybe I wanna add that sort field.\u003C/p>\u003Cp>We'll use that to, control the order of the articles. You know, we're not in documentation or like a help center or something. You don't necessarily wanna do like chronological order based on date. You wanna do, an order that you specifically set. So I do that.\u003C/p>\u003Cp>That should have also created the corresponding field inside articles, so now we have that set up. And then what I'm gonna do now that we're here in articles, we'll create that many to many relationship. I Was trying to look for another name there, wasn't I? Let's call this tags. The related collection will be tags, and we don't wanna allow duplicate tags.\u003C/p>\u003Cp>We'll show a link to this. There's a lot of other settings that we control here. You know, how many different items show in a page when you're looking at a lot of them. You shouldn't have probably more than 15 tags per page, but if you do, kudos. Alright.\u003C/p>\u003Cp>For our relationship tab here, I always open up advanced so I can see what's going on. You could see now that Directus is gonna create this junction collection for you. I could scroll down, it'll tell you exactly what fields are gonna be created, but I'm gonna adjust that. I'm a little OCD about how I name things. So this is gonna be article tags.\u003C/p>\u003Cp>The key for the article, I'm just gonna call that article and then I'm gonna call this tag instead of tags underscore ID. You can let Directus autofill that. You don't have to add it this way. Totally up to you. Whatever you prefer.\u003C/p>\u003Cp>And then I'm gonna add this corresponding field. If I do ever want to filter articles by tags, I could go straight to the tag and get a list of articles that way as well if I add this field. And we'll add a sort just just for fun. Great. There's our interface.\u003C/p>\u003Cp>There's our display. We'll display the related values. Looking good. And And we can see that Directus has created that article tags collection for us. It's hidden by default.\u003C/p>\u003Cp>We don't necessarily need to see it. And what does this look like now? Right? So hey yo. This is the markdown editor.\u003C/p>\u003Cp>We have tags. We don't see a category. So I do wanna be able to edit that category. We'll just unhead that guy and boom. There we go.\u003C/p>\u003Cp>So now we can see a title. Here's the status. And maybe we move that category up here to the top besides status. That makes sense to me. Again, the beautiful thing here is we are updating the form that our team members are actually gonna interact with, which I think is freaking awesome.\u003C/p>\u003Cp>I don't wanna hide that. I just wanna make it half width. Got a little carried away there. Move some of these other ones out of my way. Alright.\u003C/p>\u003Cp>So now we've got category. Got a status, category, title, slug, content, and then tags if we wanna apply those. What does this actually look like? Looks pretty good. There's our API.\u003C/p>\u003Cp>Cool. That's the category it's in. Here's the title. Here's the slug. There's the tags.\u003C/p>\u003Cp>We can create a new tag. Let's call this, reference or feature. We can call this items. That makes sense. Great.\u003C/p>\u003Cp>We'll hit save and stay. This is not looking nice. I don't like this. This is the UUID for the tag. So let's just go into here and I can fix this two ways.\u003C/p>\u003Cp>I can go into my tag and add a default display template. I think that should take care of it. We'll maybe do the same here for our articles. Just anytime we're using this, it should default. Okay.\u003C/p>\u003Cp>So now I can see the tags. We have items there. Looks much better. Much, much better. I still see, like, category here.\u003C/p>\u003Cp>We probably need to fix that as well just because it's gonna drive me crazy. So that is our display template here. We just wanna make sure we're displaying related values. If I don't populate that field, it should show, like, the default. Why is it not doing that?\u003C/p>\u003Cp>Okay. I'll just fix it. Just let me fix it. Alright. We want the title here.\u003C/p>\u003Cp>Let's add the title there. Make a liar out of me. Okay. So now I can see the category. I can rearrange the fields up here.\u003C/p>\u003Cp>This looks good. There's the category this belongs to. Maybe that's second, third, etcetera. Yeah. I could filter those by sending value, whatever I wanted.\u003C/p>\u003Cp>Cool. If I open up the category, I could see all the articles within it and this may be the best way to manage my different articles. Right? So eighteen minutes in, we've got the skeleton of our help center back end. Right?\u003C/p>\u003Cp>This looks nice. We've got the ability for someone to edit this content, in a nice editor and riggy jig, away we go. Right? So the other component to this, we need a front end. Right?\u003C/p>\u003Cp>And I found this beautiful docs template, from mister Tony Tony Zhang. Tony, have you ever watched this? This is a pretty cool ShadCn view docs template that you've created. It looks great. So you've got, you know, kind of the standard docs vibe going on.\u003C/p>\u003Cp>You get a lot of components already built into this thing. So let's just go to GitHub. And take a big risk here on hundred apps, hundred hours. I have not messed with this. It looks like you've got, like, a starter here, but we're gonna, like, change some of the nuts and bolts of this.\u003C/p>\u003Cp>So, we're gonna abandon my usual Nuxt starter application that I have. And let's just open this up. We're gonna go to the terminal, git clone, shad c n docs nuxt. I think I'm in the right spot. Maybe not.\u003C/p>\u003Cp>Maybe I wanna do git clone, CD and then git clone. Let's back up one directory. Looks like it was already in the help center directory. Did that what did we get here? Trash.\u003C/p>\u003Cp>Oh, that's in the trash. Let's open a new terminal instance and solve for that problem. Git clone. Okay. So now we have shad c n docs nuxt.\u003C/p>\u003Cp>We'll open this up in the terminal. P n p m I. Let's get this thing fired up. And let's just kind of inspect what's going on here. I see an app dot vue file.\u003C/p>\u003Cp>That's kind of standard for a Nuxt app. We have an app folder that controls router options. Looks like that is, like, controlling for, like, a sticky header or something like that. This is cool. And then when you see, like, we have oh, that's my own app.\u003C/p>\u003Cp>Come on, guy. That's your standard Nuxt app that you have. Right? Why are you doing that? So then we have something like pages.\u003C/p>\u003Cp>Cool. So this is using Nuxt Content. Nuxt Content is a really cool, headless kind of content. So basically, hey. We can use markdown, and allows us to use Git as a CMS, basically.\u003C/p>\u003Cp>File based CMS. That's what I was struggling with. They've got this MDC format though, which is kind of interesting where I can, like, use a component like this, like hero, and it will generate this. It'll actually generate stuff like cards for you using kind of a markdown familiar syntax, which, again, pretty cool for developers. You know, nice for content editors that they can create like a complex like looking card or something in here.\u003C/p>\u003Cp>Could get confusing, maybe not. Let's run with it and see. But we've got this thing installed. Let's just fire up the dev server and see what we can get done with this thing. Alright.\u003C/p>\u003Cp>So we'll go to local host. And we're developing. We're developing. Looking good. Zoom out and direct us a little bit.\u003C/p>\u003Cp>Oh, that's not what we want. Come on, baby. Come on. That first time's a little bit slow. Don't be shy.\u003C/p>\u003Cp>Okay. So this looks pretty good. We've got an index here. This is a content renderer. I don't know where this is picking up the content from.\u003C/p>\u003Cp>I'm assuming it's coming from this index.md thing here, and you can kinda see what's going on. This is the home page. We've got effortless and beautiful. We'll just change this. Bryant's hundred apps, hundred hours, starter, help center.\u003C/p>\u003Cp>Using cursor here. It's got nice auto complete. I I that's probably the main reason that I love it. It's a little more intelligent than, like, the Copilot for stuff that I'm already gonna be typing. This is a help center, but with a hundred apps, hundred hours starter.\u003C/p>\u003Cp>Great. We don't need this iframe. Cool. We're gonna get started. There it is.\u003C/p>\u003Cp>Great. We could leave the buttons. Cool. This is gonna be what? To where do we wanna navigate to?\u003C/p>\u003Cp>Let's go to slash, what was our doc that we had? Right? We've got accessing items that is that's what we'll do. Right? Accessing items.\u003C/p>\u003Cp>If we remove this link to GitHub, we save it. Something broken. I broke something. Why is this not working right? Actions.\u003C/p>\u003Cp>Okay. There we go. That's gonna take us to accessing items. That's a four zero four. Bummer.\u003C/p>\u003Cp>No big deal. Alright. So this got this content directory that we're pulling from. Okay. Great.\u003C/p>\u003Cp>We can see it's using that that Nuxt content here. Nice feature. Great. I'm just gonna like slap, I'm gonna copy the index file, honestly, from my other starter that I have here and I'm just gonna stick this in index because I'm lazy. Alright.\u003C/p>\u003Cp>So now you can see this is my standard starter index file that we'll see. I'll just remove that. Hundred apps, hundred hours. NUC starter. Boom.\u003C/p>\u003Cp>There it is. Great. Cool. But I should probably have a button actually to go to one of the individual pages, though. Right?\u003C/p>\u003Cp>Let's keep that. Accessing items. Get started. And I'm doing this because I don't want the actual Nuxt content pieces of this. It's okay.\u003C/p>\u003Cp>Nuxt u button. What? Nuxt link? Or should it be UI button? UI button.\u003C/p>\u003Cp>Where is our where is our UI button? There's our UI button. What other props do we have as a button? As a as Nuxt link? Can we do that with this thing?\u003C/p>\u003Cp>Will that actually work? Is it gonna like that? Atrial? Why is it not it's not liking that at all. I don't know.\u003C/p>\u003Cp>I don't know why. What if we just do a regular a? Okay. There's our tag. Obviously, that's gonna hit us with like a full page reload, but no worries.\u003C/p>\u003Cp>Alright. So what we're gonna do, I'm gonna create a Directus plug in for this, Directus.ts. And to save myself time, invoking rule number two, use whatever you have at your disposal. I've already got a sample Directus plug in in my starter. So we'll just pull this across to plug ins.\u003C/p>\u003Cp>Alright. Now if I can go full screen on this. I should put it in the wrong window, Alright. So first off, we need to install the Directus SDK. I'm not gonna use cookies here.\u003C/p>\u003Cp>So do we really need authentication? No. We don't need that from the SDK. Let's not even use real time or read me. We're just gonna use the REST API.\u003C/p>\u003Cp>Gravy. I'm gonna get rid of this bit about authentication and this bit about that. And all we're gonna do here, pretty easy, pretty simple, we are fetching a direct as public URL from the Nuxt runtime config. We are creating a directus client and then we're providing that back to the Nuxt application so we can access that in multiple places. So now we're gonna install directus SDK.\u003C/p>\u003Cp>Solid. And I'm gonna find the Nuxt config for this file. And then we're gonna add, probably need an env variable. Is there a dot env file already? There's not.\u003C/p>\u003Cp>We'll call this Directus URL. That's gonna be HTTP local host 8055. That's just what I'm running on here. Great. And then I'm gonna find what's looking for our runtime config.\u003C/p>\u003Cp>Public, direct us URL. You can see this is already picking this stuff up for me. Another nice benefit of cursor. And then so we've stripped out. We didn't really strip it out, but let's strip this out.\u003C/p>\u003Cp>We don't need that. For our slug, what do we wanna do? What do we wanna do? We've got, why is script set up not at the top? This is again, just my OCD kicking in.\u003C/p>\u003Cp>There's some lint rules set up here. E s lint config. Delete. View block order. Oh, let's change that guy.\u003C/p>\u003Cp>Alright. Is that gonna fix this now? Script setup. Script. Okay.\u003C/p>\u003Cp>Alright. Changing some rules there. We're we're cooking with gas now. Alright. So we could see what's going on here.\u003C/p>\u003Cp>Like, this is like pulling this content in from markdown files itself. That's I don't wanna do that. I'm not gonna do it. But now we got this nice directus plugin that we can use and that'll be directus URL. It should be a string.\u003C/p>\u003Cp>Let's just fire this up. There's one other thing that I'm going to add to this and it is the I I know because I've worked with this thing before. It's the Nuxt MDC package, which is basically doing some of the heavy lifting under the hood as far as, like, the taking the MDC format and, you know, generating that AST that we can then use to render with. So, we'll p m p m I at Nuxt MDC. And you can see here there is like this parsed markdown or parse markdown function that we could call.\u003C/p>\u003Cp>Then we can render that markdown like the body and the data format. Alright. So we've got those two things. Let's fire up this dev server one more time and we'll see what we could do as far as a slug. Right?\u003C/p>\u003Cp>So now we're gonna do something like this where I'm gonna import read items from the direct Us SDK and I'm also gonna pull in that Directus client from Nuxt app. So the SDK is very modular, so I don't have to import all the methods that I'm not actually using. Right? And Nuxt will do some fancy, tree shaking magic for us based on the the individual routes. So should be all great.\u003C/p>\u003Cp>What we're gonna do here, we'll do something like this. We'll do constant data and then we're gonna do await, use async data. Okay. So we've got this async data composable from Nuxt. We're gonna give this, we want let's also pull in the route for this as well.\u003C/p>\u003Cp>So route equals use route. We're gonna pass this a key to use. Alright. So this will be page this will be article let's use a back tick here. Article dash route dot params.slug.join.\u003C/p>\u003Cp>Maybe. If that's an actual hooray. Okay. Here we go. This is not right at all.\u003C/p>\u003Cp>This is one of those times that you may not want to totally trust. We're gonna return read item no. We're not gonna return that. We're gonna return directives dot request. Read items.\u003C/p>\u003Cp>We're gonna get the articles. And then we can define a query. Right? So our query is gonna look like this. We got the filter, and then the slug is what we wanna filter on.\u003C/p>\u003Cp>And we want that to be equal to route dot params dot slug dot join. I'm I'm not even sure we actually need that. I can't remember the format that Nuxt is gonna give this to us in. We could call this actual data page. And then somewhere in here, this is where this is read items, we want to do a transform on that async data call.\u003C/p>\u003Cp>Transform. Data, data zero. So we're gonna get the first item in that array. This looks good. Directus, directus dot request.\u003C/p>\u003Cp>Data page. Script setup tag. What am I missing? Did I goo something? Directus request would read items.\u003C/p>\u003Cp>Oh, I didn't miss something. There we go. I think that gets us where we wanna be. Alright. So now let's just open this up, test this out.\u003C/p>\u003Cp>Okay. Unexpected token, define page components. Let's just ignore all of this baloney right now. Got page, invalid URL. Well, that's not great.\u003C/p>\u003Cp>E n v, direct as URL. K. Nux config, direct as URL. I don't need all of that. Direct as URL.\u003C/p>\u003Cp>Oh, that's in the wrong config file. Let's just copy that. Nuxt config, shad c n docs. Cool. Why is that not showing as directories URL as string?\u003C/p>\u003Cp>I Wonder why that's not showing an error here. TypeScript error. Nux config. Wrong Nux config. Unexpected use of the global variable.\u003C/p>\u003Cp>Why is that unexpected use? Fix with AI. What does it come back with? Import dot Neta dot e m v. That's not gonna be right.\u003C/p>\u003Cp>PNP m dev. Maybe that's where we didn't have that the first time we started the dev server. Not a %. Articles, filters, slug, param. And we can even do let's just console log the route dot params dot slug.\u003C/p>\u003Cp>Slug. Okay. So now we could start to see what the problem is here. Slug needs to be does need to be joined. Slug.join.\u003C/p>\u003Cp>Dodger, hon. Slug accessing items. Why can't we fetch that page? What's going on? There's our slug.\u003C/p>\u003Cp>We could see the slug. Great. Why can't we fetch this actual page, right? If we navigate, we go back. We should see, like, a fetch request here from Directus.\u003C/p>\u003Cp>Twenty minutes. How are we doing? How are we doing? We're not doing great, Brian. Not great.\u003C/p>\u003Cp>Alright. We're we should see a call to direct us somewhere. Local host. 8055. Not seeing it.\u003C/p>\u003Cp>Alright. Accessing items. Oh, it's because we're not using a link on that index page. Server side rendering. Fun stuff.\u003C/p>\u003Cp>So is there a link in this? Links button link button link button link to okay. So we should use button link, content button link. What component does that get us? Does that get us where we wanna go?\u003C/p>\u003Cp>Kind of? Kind of not? So we see a lot of stuff coming from, like, the Nuxt content API. We don't really want any of that business. Don't want that smoke.\u003C/p>\u003Cp>How can we strip all this out? Right? Can I just is this gonna can all this? This is gonna break a bunch of stuff, isn't it? Umami client, I don't want that either.\u003C/p>\u003Cp>Slug. Okay. If I just trash all of content, what happens if I disable Nuxt content entirely? Nuxt image, you use Nuxt. Chat c n Nuxt.\u003C/p>\u003Cp>This could go way wrong. It's always fun, though. Right? Let's just remove content. You where are we using use content helpers at?\u003C/p>\u003Cp>Right? I guess I should be looking at these individual things going on here. Use content helpers. Where's that at? Aside.\u003C/p>\u003Cp>So we gotta decide. We don't even care about that. What does our where are our layouts? Do we have a layout? Components, where where are you?\u003C/p>\u003Cp>App dot view. Layout header, layout aside. Let's just remove the aside. What what's in the header? Layout header, Use config.\u003C/p>\u003Cp>Okay. Main setup. Use content headers. Search dialogue. Come on.\u003C/p>\u003Cp>Why did you do that, Brian? That doesn't make sense. Use config. It's gonna break. Header, main, new key from value path.\u003C/p>\u003Cp>Page dot value dot main. Now if we just strip that out, what what is left? So many fun things here. This is a good reason not to use your own template, or this is a good reason to use your own template. Where else are you use content helpers?\u003C/p>\u003Cp>Don't want you. Where is search dialogue being used at? Search button layout, search dialogue. Search button. We'll just remove that.\u003C/p>\u003Cp>Can I actually get something loaded? Stressful. Stressful. Stressful. What are we working with?\u003C/p>\u003Cp>Nineteen, twenty minutes. So we're back to this slug catch all route. Can we even console log the page? Let's see what we're getting back from the actual page here. Looks like we're struggling to not render any of this, so I'll just fall back on one of my popular patterns that you'll see in a bunch of other episodes of 100 apps, one hundred hours.\u003C/p>\u003Cp>We'll just do this. Div. Okay. Cool. So now we can actually see that we are getting some content.\u003C/p>\u003Cp>It's just that we're not rendering the content properly. Bada bing bada boom. That's a problem. Right? Alright.\u003C/p>\u003Cp>So now let's hop into this Nux MDC business where, hey, we're getting some markdown here from something. Can we just use this n d c component to render this markdown? Alright. So I'll go here. This will be page dot it's not body.\u003C/p>\u003Cp>It's page content. What will that actually render, if anything? View, hydration, mix, match, can't find smart icon, can't resolve, KBD. Do we see a MDC component? No.\u003C/p>\u003Cp>I don't. And why? Because we did not add this to that. So we want at nuxt js slash mdc. I think that's correct.\u003C/p>\u003Cp>Nuxt mdc module. We're gonna add that to our modules. P m p m dev. We'll restart this guy. Oh my gosh.\u003C/p>\u003Cp>Did I put this? Okay. Don't do what I did. Do not put this in the same directory as your normal stuff. Ugh, I feel like an idiot.\u003C/p>\u003Cp>Okay. There we go. Fire this up. PnPM dev, we'll run it again. We'll wait for Nuxt to run this out.\u003C/p>\u003Cp>And is this actually going to work the way we want it to once it spins all of this up? We are cruising on seventeen minutes. Can we actually show these files? Bada bing, badda boom. Cannot find compose.\u003C/p>\u003Cp>Where is this coming from? Cannot find module composer. Shadcian YAML. Shadcian docs YAML. Package YAML.\u003C/p>\u003Cp>Types index, run model v runner. What are we doing? Theme, CSS, directory. Nuxt instance is unavailable. P m p m I.\u003C/p>\u003Cp>When all else fails, just blow away all of your Nuxt stuff. What have happens if we do the node modules? PMPMI. Let's try this one more time. Or it could just be that I've totally broken this thing by stripping away the Nuxt content piece.\u003C/p>\u003Cp>Probably would have been better off just starting with our Nuxt boilerplate that I already had. Not saying this is not a good piece of kit, this Shadc and Nuxt template. Just saying if you're gonna do this, don't try to learn that and do this at the same time. Trying to think of what this would even be. That's the Nux icon TS config base URL.\u003C/p>\u003Cp>Hopefully, this thing will spin up now. Hundred apps, hundred hours, starter. There's that. It's not finding a smart icon. I don't know what the smart icon is.\u003C/p>\u003Cp>Not really super concerned about it. Alright. So here's the page. We could see this is the value that we're getting from the page. Failed to load the resource.\u003C/p>\u003Cp>So it's not actually rendering anything. Right? So that part of it sucks. Don't like that. Let's actually try this import parse markdown.\u003C/p>\u003Cp>So if we get the content, we'll do page dot value dot content. Should we even use, like, a computed prop for that? You shouldn't put side effects in in that. Right? Page value, content, slugs.\u003C/p>\u003Cp>Let's try console dot log content. What if we do console dot log page dot value? Just see what we're getting here. We want to await that. Wait that.\u003C/p>\u003Cp>What is this? Man. TS config. Build transpile. I'm not gonna I'm not even gonna use Shiki, but it's probably gonna throw a fit because we've got some components inside this thing that are using it.\u003C/p>\u003Cp>The fun of using someone else's template. Why? Where is this even coming from? If we search all inside here, YAML, Nux config, app config. So there's a lot of this in the app config.\u003C/p>\u003Cp>It require Nuxt YAML. Where is YAML? Is that it? Composer dot JS. Module helpers, vite node, types node, use config, editor config.\u003C/p>\u003Cp>Yeah. This is this is wack, man. Alright. Pivot on the fly. Bad Chad c n Docs Nuxt.\u003C/p>\u003Cp>Just delete you, sir. Now, again, this looks like a a great template. I just don't have enough time. I've got eleven minutes here. So we are getting pretty desperate at this point.\u003C/p>\u003Cp>We are gonna do I guess I shoulda took what I had before we just ripped it all out. Right? You live, you learn. V base script. I want script setup lang t s.\u003C/p>\u003Cp>Alright. Now alright. So we go to this slug. That's gonna be inside the pages directory. We don't really need auth, but let's go back to what we had.\u003C/p>\u003Cp>Pnpmi at nuxt js slash mdc. That should get us what we need here. I've already mistakenly put that into the Nuxt config. And here we're gonna use routes. Use routes.\u003C/p>\u003Cp>Import. Read items. Read items from direct us Nuxt. Then we'll use constant data of the page. We will await use async data.\u003C/p>\u003Cp>Give this a page, page routes dot params.slug dot join slash. Great. Okay. Then we've got the actual function we're gonna return. Return, read items, pages, filters, slug, not even close.\u003C/p>\u003Cp>Directus dot request, read items, pages. Still not even close. Boys, come on. Cursor is not not savvy on this. Filter.\u003C/p>\u003Cp>It's slug. You're just getting in the way at this point, aren't you, friend? And then we will pull in direct us from Nuxt app. We hit page. This gives us what we need.\u003C/p>\u003Cp>P m p m dev. Parse markdown. Pull that in. Alright. So now we want to parse the markdown content.\u003C/p>\u003Cp>Console log page. Console log content. We'll just do pre content. What is that actually gonna render, if anything? Oh my gosh.\u003C/p>\u003Cp>Failed to resolve import blah blah blah. Does this actually work? Gosh. Fix your template, homie. Fix your template.\u003C/p>\u003Cp>Alright. I'm sure you guys are having fun following along with this disaster show here. PMPMI. Delete. PNPM dev.\u003C/p>\u003Cp>This is probably like a type error, I'm assuming. Fingers crossed. Or we might just have to hang it up on this one. I hate to be defeated. I really just wanna show some of this content, and I wanted to try the, like, the actual components where you can do MDC in here.\u003C/p>\u003Cp>Oh my god. Failed to load. BMPM lock. Delete. Here's what we'll do.\u003C/p>\u003Cp>If this doesn't work, I'm done. Nuxt. Node modules. Delete. Nuxt.\u003C/p>\u003Cp>Do we have the MDC in here? Nuxt MDC. Where are you? There it is. Nuxt MDC.\u003C/p>\u003Cp>Alright. Let me fire this up one last time. We got this. You got this next. Come on, baby.\u003C/p>\u003Cp>We've already got our plug in here that should be working for us. We can strip all of this out, just comment it out, basically. We don't need that. Don't need any of that. Okay.\u003C/p>\u003Cp>Everything's installed. PMPM dev. I think we need, like, a blooper reel, but maybe that would be every episode that resulted in just nonstop bloopers. Okay. So far so good.\u003C/p>\u003Cp>I don't see any major errors coming through. Cannot resolve Tailwind CSS. Are you freaking kidding me? PMPMI, Tailwind CSS at next. Sometimes it just doesn't work out in your favor.\u003C/p>\u003Cp>I think that is like 90% of modern JavaScript development is wrestling with dependencies and bundlers and config. Honestly, I spend way more time doing that than I do actually developing stuff. Non pojos. Does this have to have Nuxt Content installed? PMI Nuxt content.\u003C/p>\u003Cp>What is the name of this package? Content dot Nuxt. At Nuxt content, I'm assuming, releases. You can start with a fresh I wanna start add to a project, add Nuxt content. I swear if this is it, I'm gonna be so mad that you just have to install this thing.\u003C/p>\u003Cp>I thought I was being clever showing you guys this Nux MDC stuff. I've messed with this before. I've got it working previously. Obviously, a little little rusty. Alright.\u003C/p>\u003Cp>So let's say we do wanna do, like, this alert thing. What is this thing doing, man? What? Where is Nuxt YAML? Where the hell is this coming from?\u003C/p>\u003Cp>Can I find Compose? Compose.js. Where where is this even at, man? Composer requires to CJS loader YAML, feet runner. I'm not loading any YAML.\u003C/p>\u003Cp>YAML. Do I have something in the content directory? No. Freaking no, man. If I remove this parse markdown, does that fix it?\u003C/p>\u003Cp>Page dot contents. Okay. Yeah. So it renders something here. Console log.\u003C/p>\u003Cp>Page is undefined. That could be why. Not even getting a page. That's why you don't trust, don't trust AI, friends. Page dot it's not even page.\u003C/p>\u003Cp>It shouldn't be page. But page return request direct us articles filter slug equals route dot params .slug.join. Okay. So now we can see the page. We need to go back and add our transform option.\u003C/p>\u003Cp>Transform. Get our data. Hey, yo. There's our content. What do we got?\u003C/p>\u003Cp>We got a minute and forty seven seconds left. Amazing. Just freaking amazing, honestly. H one page dot title. Class text three x l, font bold.\u003C/p>\u003Cp>Let's add a container to this. Right? That'll give us our page. Amazing. Is that really what it was?\u003C/p>\u003Cp>Now is this actually gonna work? Content. What if we show, like, MDC render value equals content. No. It's it's still not gonna shove that down, is it?\u003C/p>\u003Cp>Well, well, well, well, mister Bryant, it does not like you. Anyway, we got remotely close. Nah. We really didn't. We've got the help center, like, part of it set up.\u003C/p>\u003Cp>Didn't even get into any of the, like, fun MDC component stuff. Roll credits, guys. Yeah. Recapping this, it was a bit ambitious to dive into a template that I had never used before and was totally set up for local markdown files. And I think I could wrestle that in an hour and actually show you how to render Vue components from content, delivered via the CMS.\u003C/p>\u003Cp>But, hey, sometimes you win, sometimes you lose. Just gotta keep rolling the dice. Alright. That's it for this episode of hundred apps, hundred hours. We'll catch you on the next one.\u003C/p>\u003Cp>Thanks for joining.\u003C/p>","What's up peeps? Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie for Directus. Today, we've got a pretty cool episode, I think. Yeah. I've seen this come up time and time again in our community and some of the questions. You know, most documentation sites or help centers, at least in the developer community, they're all static markdown files, which is great. As a developer, I love markdown plain text formatting in a nice easy to read way, easy for me to author. Content editors, they don't necessarily love markdown. So today, we're going to be building a help center. If you're new to 100 apps, one hundred hours, there are two rules. We have sixty minutes to plan and build an application, an idea, a clone, whatever, no more, no less. And number two, use whatever we have at our disposal. So with that, let's fire sixty minutes on the clock and go. So I usually start every episode just trying to figure out what type of functionality we're looking for here. So I want a documentation site, a help center slash documentation site where let's do this on a new line, where content editors can edit within a CMS and not necessarily text files or markdown or whatever. Right? Those are the main requirements here. If we were to put down, like, a stretch goal on this, I would say, like, IT and IN support could be a we'll call it a stretch goal. Sounds good? Sounds great. Alright. So as far as what we want out of our data model here. Right? When I think of a help center, I think of categories. So I think of what section do these belong in, and I think of articles. So category has what? It has a name. It has articles within it. So there's a relationship there. The articles belong to a category. Could there be multiple categories or multiple could an article live in multiple categories? I would say not. I would say that's probably the realm for our friend tags. Right? Or topics or something like that. Right? I can have a mini to mini relationship. So we'll do some nice looking arrows. Great. Great looking arrows. So we have multiple tags on an article, but an article belongs to a single category. Right? So we have a title for the article. We have a slug and then we have content. Now this could be markdown if you prefer that. It could be HTML from a WYSIWYG editor. However we wanna do that, we'll tackle that as we dive in. Alright. So before we even do the front end, let's start on this inside Directus. Now I've got a nice kind of front end template picked out for this, but, let's start back end first with our data. Right? The first thing we're gonna create, let's let's tackle categories. Right? So we'll do categories. We could call these collections. We call a table inside direct us a collection, so that could be confusing. Let's just call it a category. Great. We'll give it a UUID and we'll hit save. Do I want any of these other things? We probably want sorts for these. You know, I could decide not to publish an entire category or not. I guess that's okay. Let's give this a name. Category name, makes sense. Article title, I am we could go with title across the board. I guess that makes more sense. Alright. So let's go ahead and start cleaning up the form a bit. I like title, status. Do we have a slug for the category? Probably. Forgot to put that in there. We can even add nice little icons in Directus. Makes it super simple. We'll just add a little link. I'm gonna open up the interface for this and just Slugify this. Make my Slugify option using my little mouse Pose tool here. We'll trim the start and end in case there are white spaces, and let's make this mono space font. Yeah. This is gonna look amazing. Right? We'll use mono space font for the display. That just controls how it looks in the different directus layouts. We have a title, a slug. We have a category. What else do we want? We wanna have an icon. Right? Cool. Icon for this category. Can't get rid of the designer OCD stuff that I still hang on to. Cool. That looks great for categories. Now let's set up our articles. It doesn't look great. Forgot an icon for the actual category itself and a display template. Great. Okay. So now we have categories. I could go in and create a new category. Let's call this API reference. Right? API. And do we have an API? Yeah. Your standard API icon. We'll just go ahead and set this to published. Great. We have a category. I could query this on the front end. Items slash categories. Boom. I've got a REST API. Magic happens. Sparkles fall from the sky, etcetera. Alright. So we've got categories. Let's add articles now. So again, I'm going to caution against using something like slug here as the primary key field because if you ever need to change that slug, you cannot change the primary key value for a record because of all the relationships that are attached to it. Could get very messy. Right? So, what we'll do here, let's add created on, created by. These system fields here or these optional system fields are helpful. Created by you can also rename them as you see me doing here. Updated at, created at, updated at, updated by that'll be the user. Cool. You can use them as is. You can leave these out. You can add them back later. It's just a shortcut for you. Alright. So let's pick the article icon. Is there an article icon? What do you got for me? Directus. That looks pretty good for an article. We'll command s to save and stay. And let's start fiddling around with this, right? Again, we'll use a title for the article. Cool. Gravy. One of the other shortcuts that you can use in Directus, if you have a field that you want from another collection per se inside your data model, I can just go to duplicate. So I've already got the slug set up how I want it. We'll just dupe that across to the articles collection. Alright. And categories will probably be above that. So here I can see my slug field. We'll just set that to half width. What else did we have here? We had content. Right? So how are we gonna set up content? Do we want WYSIWYG? Do we want Markdown? It could go either way. Let's lean Markdown first just to see what this looks like. Hit save. And basically behind the scenes, this is just creating a text column inside the database. So, you know, if I wanted to go back later and adjust like the actual interface, what the user interacts with, I can change that from markdown to WYSIWYG very easily. Or if you're like me, you might use even plug something like Tiptap in here through our directus marketplace. Great. K. Now we've got tags. Let's go in and create our tags. Great. Generate a UUID. What is the tag? Do we have a sort for the tags? We don't really have any of this tags. We'll use a hashtag. What's the tag? Name of the tag, title of the tag? We'll just use title. Again, naming things is always the hardest thing. I think there's like a receipt option that kinda looks like a tag. Does that bookmark maybe? Kinda looks like a tag. Kinda also looks a lot like a bookmark. This does not look like a tag. Oh, that's a Pentagon. Pin tag on. Yeah. Cool. Let's use that. Sounds great. Alright. So now we have a tag. I don't know that we need a a slug on this. You know, you might have, like, grouping for tags later. But that's kind of the basic of this data model. Right? Now let's let's add an article to this, and we can quickly see what we're working with. Accessing items. Items. Accessing items. So there's our slug. We can publish this guy. We can go in and add some markdown. Hey, yo. This is a markdown editor. It edits markdown. Wild, I know, right? And then I could preview that. We could save this. Again, we can just, like, look at this URL. We can replace items with the admin. So here we can see this data. This is because we're actually logged in. When we send this request, it is sending our token in the cookie. If I were to do this in, like, a incognito window, you'll see that I I get no access. No access to that. We could solve that by going in and updating our access policies and and or our roles. So, by default, Directus loads two roles. There's a public and an administrator role. We've got our access policies here which are, more granular. Right? I can have I can attach 35 different access policies to a single role if I want to. But if we look here, the data that is available without authentication is absolutely zero. Absolutely nothing because we want you to be secure. So we'll just go in, we'll add tags. We're probably gonna want files if we choose to upload them. So we'll add read access to files, add read access to tags, add read access to categories and articles. The one restriction I'm gonna put on articles is that we only wanna show published articles. Pretty straightforward. I guess we could do this to categories as well because we do we have a status? Yes. We only wanna show published categories. Now if I were to test that out, we just create a this is unpublished article. This is a work in progress. That's what we'll call it. This is WIP. Great. I got two articles now. If I open up that incognito window, paste that same URL except we drop the ID, you could see I'm only getting a single article that is published. If I were to change the WIP article to published, boom, boom, fetch. Yeah. There we go. I can now see that WIP article. Great. Permissions works. We have a REST API. We have what we need to proceed. So, the thing we are missing here is our relationships. Right? We don't have a relationship between articles and tags and categories and vice versa. So let's solve for that. Right? First thing, we need to define kinda the way those relationships work. There is a one to many relationship between categories and articles and a many to many relationship between our tags. So I could do this. I could go into our categories. We'll look for that one to many relationship. We'll call this articles. The related collection is also gonna be called articles. So the key is the name of the field inside the categories collection, the related collection here, and then we have a foreign key, which will be the category, probably a a nice naming convention here, singular, plural, fun stuff. So the category is only going to be a pointer back to the categories collection. And for the displayed template, we could show the title. Great. Maybe we wanted to do show if this is actually published or not. I think that's a good way to handle it. And we'll show a link to this item. And if I wanted to drill into this, I can go open the advanced field and we could see here's the related collection. You know, maybe I wanna add that sort field. We'll use that to, control the order of the articles. You know, we're not in documentation or like a help center or something. You don't necessarily wanna do like chronological order based on date. You wanna do, an order that you specifically set. So I do that. That should have also created the corresponding field inside articles, so now we have that set up. And then what I'm gonna do now that we're here in articles, we'll create that many to many relationship. I Was trying to look for another name there, wasn't I? Let's call this tags. The related collection will be tags, and we don't wanna allow duplicate tags. We'll show a link to this. There's a lot of other settings that we control here. You know, how many different items show in a page when you're looking at a lot of them. You shouldn't have probably more than 15 tags per page, but if you do, kudos. Alright. For our relationship tab here, I always open up advanced so I can see what's going on. You could see now that Directus is gonna create this junction collection for you. I could scroll down, it'll tell you exactly what fields are gonna be created, but I'm gonna adjust that. I'm a little OCD about how I name things. So this is gonna be article tags. The key for the article, I'm just gonna call that article and then I'm gonna call this tag instead of tags underscore ID. You can let Directus autofill that. You don't have to add it this way. Totally up to you. Whatever you prefer. And then I'm gonna add this corresponding field. If I do ever want to filter articles by tags, I could go straight to the tag and get a list of articles that way as well if I add this field. And we'll add a sort just just for fun. Great. There's our interface. There's our display. We'll display the related values. Looking good. And And we can see that Directus has created that article tags collection for us. It's hidden by default. We don't necessarily need to see it. And what does this look like now? Right? So hey yo. This is the markdown editor. We have tags. We don't see a category. So I do wanna be able to edit that category. We'll just unhead that guy and boom. There we go. So now we can see a title. Here's the status. And maybe we move that category up here to the top besides status. That makes sense to me. Again, the beautiful thing here is we are updating the form that our team members are actually gonna interact with, which I think is freaking awesome. I don't wanna hide that. I just wanna make it half width. Got a little carried away there. Move some of these other ones out of my way. Alright. So now we've got category. Got a status, category, title, slug, content, and then tags if we wanna apply those. What does this actually look like? Looks pretty good. There's our API. Cool. That's the category it's in. Here's the title. Here's the slug. There's the tags. We can create a new tag. Let's call this, reference or feature. We can call this items. That makes sense. Great. We'll hit save and stay. This is not looking nice. I don't like this. This is the UUID for the tag. So let's just go into here and I can fix this two ways. I can go into my tag and add a default display template. I think that should take care of it. We'll maybe do the same here for our articles. Just anytime we're using this, it should default. Okay. So now I can see the tags. We have items there. Looks much better. Much, much better. I still see, like, category here. We probably need to fix that as well just because it's gonna drive me crazy. So that is our display template here. We just wanna make sure we're displaying related values. If I don't populate that field, it should show, like, the default. Why is it not doing that? Okay. I'll just fix it. Just let me fix it. Alright. We want the title here. Let's add the title there. Make a liar out of me. Okay. So now I can see the category. I can rearrange the fields up here. This looks good. There's the category this belongs to. Maybe that's second, third, etcetera. Yeah. I could filter those by sending value, whatever I wanted. Cool. If I open up the category, I could see all the articles within it and this may be the best way to manage my different articles. Right? So eighteen minutes in, we've got the skeleton of our help center back end. Right? This looks nice. We've got the ability for someone to edit this content, in a nice editor and riggy jig, away we go. Right? So the other component to this, we need a front end. Right? And I found this beautiful docs template, from mister Tony Tony Zhang. Tony, have you ever watched this? This is a pretty cool ShadCn view docs template that you've created. It looks great. So you've got, you know, kind of the standard docs vibe going on. You get a lot of components already built into this thing. So let's just go to GitHub. And take a big risk here on hundred apps, hundred hours. I have not messed with this. It looks like you've got, like, a starter here, but we're gonna, like, change some of the nuts and bolts of this. So, we're gonna abandon my usual Nuxt starter application that I have. And let's just open this up. We're gonna go to the terminal, git clone, shad c n docs nuxt. I think I'm in the right spot. Maybe not. Maybe I wanna do git clone, CD and then git clone. Let's back up one directory. Looks like it was already in the help center directory. Did that what did we get here? Trash. Oh, that's in the trash. Let's open a new terminal instance and solve for that problem. Git clone. Okay. So now we have shad c n docs nuxt. We'll open this up in the terminal. P n p m I. Let's get this thing fired up. And let's just kind of inspect what's going on here. I see an app dot vue file. That's kind of standard for a Nuxt app. We have an app folder that controls router options. Looks like that is, like, controlling for, like, a sticky header or something like that. This is cool. And then when you see, like, we have oh, that's my own app. Come on, guy. That's your standard Nuxt app that you have. Right? Why are you doing that? So then we have something like pages. Cool. So this is using Nuxt Content. Nuxt Content is a really cool, headless kind of content. So basically, hey. We can use markdown, and allows us to use Git as a CMS, basically. File based CMS. That's what I was struggling with. They've got this MDC format though, which is kind of interesting where I can, like, use a component like this, like hero, and it will generate this. It'll actually generate stuff like cards for you using kind of a markdown familiar syntax, which, again, pretty cool for developers. You know, nice for content editors that they can create like a complex like looking card or something in here. Could get confusing, maybe not. Let's run with it and see. But we've got this thing installed. Let's just fire up the dev server and see what we can get done with this thing. Alright. So we'll go to local host. And we're developing. We're developing. Looking good. Zoom out and direct us a little bit. Oh, that's not what we want. Come on, baby. Come on. That first time's a little bit slow. Don't be shy. Okay. So this looks pretty good. We've got an index here. This is a content renderer. I don't know where this is picking up the content from. I'm assuming it's coming from this index.md thing here, and you can kinda see what's going on. This is the home page. We've got effortless and beautiful. We'll just change this. Bryant's hundred apps, hundred hours, starter, help center. Using cursor here. It's got nice auto complete. I I that's probably the main reason that I love it. It's a little more intelligent than, like, the Copilot for stuff that I'm already gonna be typing. This is a help center, but with a hundred apps, hundred hours starter. Great. We don't need this iframe. Cool. We're gonna get started. There it is. Great. We could leave the buttons. Cool. This is gonna be what? To where do we wanna navigate to? Let's go to slash, what was our doc that we had? Right? We've got accessing items that is that's what we'll do. Right? Accessing items. If we remove this link to GitHub, we save it. Something broken. I broke something. Why is this not working right? Actions. Okay. There we go. That's gonna take us to accessing items. That's a four zero four. Bummer. No big deal. Alright. So this got this content directory that we're pulling from. Okay. Great. We can see it's using that that Nuxt content here. Nice feature. Great. I'm just gonna like slap, I'm gonna copy the index file, honestly, from my other starter that I have here and I'm just gonna stick this in index because I'm lazy. Alright. So now you can see this is my standard starter index file that we'll see. I'll just remove that. Hundred apps, hundred hours. NUC starter. Boom. There it is. Great. Cool. But I should probably have a button actually to go to one of the individual pages, though. Right? Let's keep that. Accessing items. Get started. And I'm doing this because I don't want the actual Nuxt content pieces of this. It's okay. Nuxt u button. What? Nuxt link? Or should it be UI button? UI button. Where is our where is our UI button? There's our UI button. What other props do we have as a button? As a as Nuxt link? Can we do that with this thing? Will that actually work? Is it gonna like that? Atrial? Why is it not it's not liking that at all. I don't know. I don't know why. What if we just do a regular a? Okay. There's our tag. Obviously, that's gonna hit us with like a full page reload, but no worries. Alright. So what we're gonna do, I'm gonna create a Directus plug in for this, Directus.ts. And to save myself time, invoking rule number two, use whatever you have at your disposal. I've already got a sample Directus plug in in my starter. So we'll just pull this across to plug ins. Alright. Now if I can go full screen on this. I should put it in the wrong window, Alright. So first off, we need to install the Directus SDK. I'm not gonna use cookies here. So do we really need authentication? No. We don't need that from the SDK. Let's not even use real time or read me. We're just gonna use the REST API. Gravy. I'm gonna get rid of this bit about authentication and this bit about that. And all we're gonna do here, pretty easy, pretty simple, we are fetching a direct as public URL from the Nuxt runtime config. We are creating a directus client and then we're providing that back to the Nuxt application so we can access that in multiple places. So now we're gonna install directus SDK. Solid. And I'm gonna find the Nuxt config for this file. And then we're gonna add, probably need an env variable. Is there a dot env file already? There's not. We'll call this Directus URL. That's gonna be HTTP local host 8055. That's just what I'm running on here. Great. And then I'm gonna find what's looking for our runtime config. Public, direct us URL. You can see this is already picking this stuff up for me. Another nice benefit of cursor. And then so we've stripped out. We didn't really strip it out, but let's strip this out. We don't need that. For our slug, what do we wanna do? What do we wanna do? We've got, why is script set up not at the top? This is again, just my OCD kicking in. There's some lint rules set up here. E s lint config. Delete. View block order. Oh, let's change that guy. Alright. Is that gonna fix this now? Script setup. Script. Okay. Alright. Changing some rules there. We're we're cooking with gas now. Alright. So we could see what's going on here. Like, this is like pulling this content in from markdown files itself. That's I don't wanna do that. I'm not gonna do it. But now we got this nice directus plugin that we can use and that'll be directus URL. It should be a string. Let's just fire this up. There's one other thing that I'm going to add to this and it is the I I know because I've worked with this thing before. It's the Nuxt MDC package, which is basically doing some of the heavy lifting under the hood as far as, like, the taking the MDC format and, you know, generating that AST that we can then use to render with. So, we'll p m p m I at Nuxt MDC. And you can see here there is like this parsed markdown or parse markdown function that we could call. Then we can render that markdown like the body and the data format. Alright. So we've got those two things. Let's fire up this dev server one more time and we'll see what we could do as far as a slug. Right? So now we're gonna do something like this where I'm gonna import read items from the direct Us SDK and I'm also gonna pull in that Directus client from Nuxt app. So the SDK is very modular, so I don't have to import all the methods that I'm not actually using. Right? And Nuxt will do some fancy, tree shaking magic for us based on the the individual routes. So should be all great. What we're gonna do here, we'll do something like this. We'll do constant data and then we're gonna do await, use async data. Okay. So we've got this async data composable from Nuxt. We're gonna give this, we want let's also pull in the route for this as well. So route equals use route. We're gonna pass this a key to use. Alright. So this will be page this will be article let's use a back tick here. Article dash route dot params.slug.join. Maybe. If that's an actual hooray. Okay. Here we go. This is not right at all. This is one of those times that you may not want to totally trust. We're gonna return read item no. We're not gonna return that. We're gonna return directives dot request. Read items. We're gonna get the articles. And then we can define a query. Right? So our query is gonna look like this. We got the filter, and then the slug is what we wanna filter on. And we want that to be equal to route dot params dot slug dot join. I'm I'm not even sure we actually need that. I can't remember the format that Nuxt is gonna give this to us in. We could call this actual data page. And then somewhere in here, this is where this is read items, we want to do a transform on that async data call. Transform. Data, data zero. So we're gonna get the first item in that array. This looks good. Directus, directus dot request. Data page. Script setup tag. What am I missing? Did I goo something? Directus request would read items. Oh, I didn't miss something. There we go. I think that gets us where we wanna be. Alright. So now let's just open this up, test this out. Okay. Unexpected token, define page components. Let's just ignore all of this baloney right now. Got page, invalid URL. Well, that's not great. E n v, direct as URL. K. Nux config, direct as URL. I don't need all of that. Direct as URL. Oh, that's in the wrong config file. Let's just copy that. Nuxt config, shad c n docs. Cool. Why is that not showing as directories URL as string? I Wonder why that's not showing an error here. TypeScript error. Nux config. Wrong Nux config. Unexpected use of the global variable. Why is that unexpected use? Fix with AI. What does it come back with? Import dot Neta dot e m v. That's not gonna be right. PNP m dev. Maybe that's where we didn't have that the first time we started the dev server. Not a %. Articles, filters, slug, param. And we can even do let's just console log the route dot params dot slug. Slug. Okay. So now we could start to see what the problem is here. Slug needs to be does need to be joined. Slug.join. Dodger, hon. Slug accessing items. Why can't we fetch that page? What's going on? There's our slug. We could see the slug. Great. Why can't we fetch this actual page, right? If we navigate, we go back. We should see, like, a fetch request here from Directus. Twenty minutes. How are we doing? How are we doing? We're not doing great, Brian. Not great. Alright. We're we should see a call to direct us somewhere. Local host. 8055. Not seeing it. Alright. Accessing items. Oh, it's because we're not using a link on that index page. Server side rendering. Fun stuff. So is there a link in this? Links button link button link button link to okay. So we should use button link, content button link. What component does that get us? Does that get us where we wanna go? Kind of? Kind of not? So we see a lot of stuff coming from, like, the Nuxt content API. We don't really want any of that business. Don't want that smoke. How can we strip all this out? Right? Can I just is this gonna can all this? This is gonna break a bunch of stuff, isn't it? Umami client, I don't want that either. Slug. Okay. If I just trash all of content, what happens if I disable Nuxt content entirely? Nuxt image, you use Nuxt. Chat c n Nuxt. This could go way wrong. It's always fun, though. Right? Let's just remove content. You where are we using use content helpers at? Right? I guess I should be looking at these individual things going on here. Use content helpers. Where's that at? Aside. So we gotta decide. We don't even care about that. What does our where are our layouts? Do we have a layout? Components, where where are you? App dot view. Layout header, layout aside. Let's just remove the aside. What what's in the header? Layout header, Use config. Okay. Main setup. Use content headers. Search dialogue. Come on. Why did you do that, Brian? That doesn't make sense. Use config. It's gonna break. Header, main, new key from value path. Page dot value dot main. Now if we just strip that out, what what is left? So many fun things here. This is a good reason not to use your own template, or this is a good reason to use your own template. Where else are you use content helpers? Don't want you. Where is search dialogue being used at? Search button layout, search dialogue. Search button. We'll just remove that. Can I actually get something loaded? Stressful. Stressful. Stressful. What are we working with? Nineteen, twenty minutes. So we're back to this slug catch all route. Can we even console log the page? Let's see what we're getting back from the actual page here. Looks like we're struggling to not render any of this, so I'll just fall back on one of my popular patterns that you'll see in a bunch of other episodes of 100 apps, one hundred hours. We'll just do this. Div. Okay. Cool. So now we can actually see that we are getting some content. It's just that we're not rendering the content properly. Bada bing bada boom. That's a problem. Right? Alright. So now let's hop into this Nux MDC business where, hey, we're getting some markdown here from something. Can we just use this n d c component to render this markdown? Alright. So I'll go here. This will be page dot it's not body. It's page content. What will that actually render, if anything? View, hydration, mix, match, can't find smart icon, can't resolve, KBD. Do we see a MDC component? No. I don't. And why? Because we did not add this to that. So we want at nuxt js slash mdc. I think that's correct. Nuxt mdc module. We're gonna add that to our modules. P m p m dev. We'll restart this guy. Oh my gosh. Did I put this? Okay. Don't do what I did. Do not put this in the same directory as your normal stuff. Ugh, I feel like an idiot. Okay. There we go. Fire this up. PnPM dev, we'll run it again. We'll wait for Nuxt to run this out. And is this actually going to work the way we want it to once it spins all of this up? We are cruising on seventeen minutes. Can we actually show these files? Bada bing, badda boom. Cannot find compose. Where is this coming from? Cannot find module composer. Shadcian YAML. Shadcian docs YAML. Package YAML. Types index, run model v runner. What are we doing? Theme, CSS, directory. Nuxt instance is unavailable. P m p m I. When all else fails, just blow away all of your Nuxt stuff. What have happens if we do the node modules? PMPMI. Let's try this one more time. Or it could just be that I've totally broken this thing by stripping away the Nuxt content piece. Probably would have been better off just starting with our Nuxt boilerplate that I already had. Not saying this is not a good piece of kit, this Shadc and Nuxt template. Just saying if you're gonna do this, don't try to learn that and do this at the same time. Trying to think of what this would even be. That's the Nux icon TS config base URL. Hopefully, this thing will spin up now. Hundred apps, hundred hours, starter. There's that. It's not finding a smart icon. I don't know what the smart icon is. Not really super concerned about it. Alright. So here's the page. We could see this is the value that we're getting from the page. Failed to load the resource. So it's not actually rendering anything. Right? So that part of it sucks. Don't like that. Let's actually try this import parse markdown. So if we get the content, we'll do page dot value dot content. Should we even use, like, a computed prop for that? You shouldn't put side effects in in that. Right? Page value, content, slugs. Let's try console dot log content. What if we do console dot log page dot value? Just see what we're getting here. We want to await that. Wait that. What is this? Man. TS config. Build transpile. I'm not gonna I'm not even gonna use Shiki, but it's probably gonna throw a fit because we've got some components inside this thing that are using it. The fun of using someone else's template. Why? Where is this even coming from? If we search all inside here, YAML, Nux config, app config. So there's a lot of this in the app config. It require Nuxt YAML. Where is YAML? Is that it? Composer dot JS. Module helpers, vite node, types node, use config, editor config. Yeah. This is this is wack, man. Alright. Pivot on the fly. Bad Chad c n Docs Nuxt. Just delete you, sir. Now, again, this looks like a a great template. I just don't have enough time. I've got eleven minutes here. So we are getting pretty desperate at this point. We are gonna do I guess I shoulda took what I had before we just ripped it all out. Right? You live, you learn. V base script. I want script setup lang t s. Alright. Now alright. So we go to this slug. That's gonna be inside the pages directory. We don't really need auth, but let's go back to what we had. Pnpmi at nuxt js slash mdc. That should get us what we need here. I've already mistakenly put that into the Nuxt config. And here we're gonna use routes. Use routes. Import. Read items. Read items from direct us Nuxt. Then we'll use constant data of the page. We will await use async data. Give this a page, page routes dot params.slug dot join slash. Great. Okay. Then we've got the actual function we're gonna return. Return, read items, pages, filters, slug, not even close. Directus dot request, read items, pages. Still not even close. Boys, come on. Cursor is not not savvy on this. Filter. It's slug. You're just getting in the way at this point, aren't you, friend? And then we will pull in direct us from Nuxt app. We hit page. This gives us what we need. P m p m dev. Parse markdown. Pull that in. Alright. So now we want to parse the markdown content. Console log page. Console log content. We'll just do pre content. What is that actually gonna render, if anything? Oh my gosh. Failed to resolve import blah blah blah. Does this actually work? Gosh. Fix your template, homie. Fix your template. Alright. I'm sure you guys are having fun following along with this disaster show here. PMPMI. Delete. PNPM dev. This is probably like a type error, I'm assuming. Fingers crossed. Or we might just have to hang it up on this one. I hate to be defeated. I really just wanna show some of this content, and I wanted to try the, like, the actual components where you can do MDC in here. Oh my god. Failed to load. BMPM lock. Delete. Here's what we'll do. If this doesn't work, I'm done. Nuxt. Node modules. Delete. Nuxt. Do we have the MDC in here? Nuxt MDC. Where are you? There it is. Nuxt MDC. Alright. Let me fire this up one last time. We got this. You got this next. Come on, baby. We've already got our plug in here that should be working for us. We can strip all of this out, just comment it out, basically. We don't need that. Don't need any of that. Okay. Everything's installed. PMPM dev. I think we need, like, a blooper reel, but maybe that would be every episode that resulted in just nonstop bloopers. Okay. So far so good. I don't see any major errors coming through. Cannot resolve Tailwind CSS. Are you freaking kidding me? PMPMI, Tailwind CSS at next. Sometimes it just doesn't work out in your favor. I think that is like 90% of modern JavaScript development is wrestling with dependencies and bundlers and config. Honestly, I spend way more time doing that than I do actually developing stuff. Non pojos. Does this have to have Nuxt Content installed? PMI Nuxt content. What is the name of this package? Content dot Nuxt. At Nuxt content, I'm assuming, releases. You can start with a fresh I wanna start add to a project, add Nuxt content. I swear if this is it, I'm gonna be so mad that you just have to install this thing. I thought I was being clever showing you guys this Nux MDC stuff. I've messed with this before. I've got it working previously. Obviously, a little little rusty. Alright. So let's say we do wanna do, like, this alert thing. What is this thing doing, man? What? Where is Nuxt YAML? Where the hell is this coming from? Can I find Compose? Compose.js. Where where is this even at, man? Composer requires to CJS loader YAML, feet runner. I'm not loading any YAML. YAML. Do I have something in the content directory? No. Freaking no, man. If I remove this parse markdown, does that fix it? Page dot contents. Okay. Yeah. So it renders something here. Console log. Page is undefined. That could be why. Not even getting a page. That's why you don't trust, don't trust AI, friends. Page dot it's not even page. It shouldn't be page. But page return request direct us articles filter slug equals route dot params .slug.join. Okay. So now we can see the page. We need to go back and add our transform option. Transform. Get our data. Hey, yo. There's our content. What do we got? We got a minute and forty seven seconds left. Amazing. Just freaking amazing, honestly. H one page dot title. Class text three x l, font bold. Let's add a container to this. Right? That'll give us our page. Amazing. Is that really what it was? Now is this actually gonna work? Content. What if we show, like, MDC render value equals content. No. It's it's still not gonna shove that down, is it? Well, well, well, well, mister Bryant, it does not like you. Anyway, we got remotely close. Nah. We really didn't. We've got the help center, like, part of it set up. Didn't even get into any of the, like, fun MDC component stuff. Roll credits, guys. Yeah. Recapping this, it was a bit ambitious to dive into a template that I had never used before and was totally set up for local markdown files. And I think I could wrestle that in an hour and actually show you how to render Vue components from content, delivered via the CMS. But, hey, sometimes you win, sometimes you lose. Just gotta keep rolling the dice. Alright. That's it for this episode of hundred apps, hundred hours. We'll catch you on the next one. Thanks for joining.","3dde94eb-edb1-47e2-b894-8261adc507c7",[605],"83401e22-c391-4741-b6e7-6d47ec52de87",[],{"id":142,"number":143,"show":122,"year":144,"episodes":608},[146,147,148,149,150,151,152,153,154,155],{"id":150,"slug":610,"vimeo_id":611,"description":612,"tile":613,"length":614,"resources":8,"people":8,"episode_number":270,"published":556,"title":615,"video_transcript_html":616,"video_transcript_text":617,"content":8,"seo":618,"status":130,"episode_people":619,"recommendations":621,"season":622},"feedback-widget","1059435866","Bryant builds an embeddable feedback widget for documentation sites that captures user ratings and comments with a decisive 4-point scale. Watch as he creates a complete system with slick animations and wraps it in an iframe-friendly package, then builds a dashboard to visualize feedback metrics with conditional styling.","c0378c84-fc0c-427c-94d2-89cc3bbe7b8b",53,"Mission: Feedback Widget","\u003Cp>Speaker 0: And we're back with another episode of 100 apps, one hundred hours. I'm your host Brian Gillespie here for Directus. And today, we are building a feedback widget. And you know what they say, if it quacks like a duck, it's probably a duck. So we'll see what the duck is going on today.\u003C/p>\u003Cp>I've got this amazing hockey jersey that, my brother-in-law gifted me a couple years ago. Rocking that today. So what are we rocking on the build today? We're rocking a feedback widget and I don't often, like, carry over builds from episode to episode. But if you watched the last episode, you probably watched me fail to try and massage a help center template into something that pulls the data remotely, pulls the content remotely from Directus as a CMS instead of local markdown files.\u003C/p>\u003Cp>Rectified that issue a little bit off scene. And today, we're gonna carry that same theme. We're gonna be build a feedback widget for the documentation site. So if this is your first go with hundred apps hundred hours, you might wanna go back and watch me fail miserably at that one. Maybe not.\u003C/p>\u003Cp>You'll still get value out of this one. You'll be able to follow along. You don't have to watch that one. But the rules are, we have sixty minutes to plan, sixty minutes to build. Well, it's just sixty minutes total.\u003C/p>\u003Cp>Plan and build. No more, no less. And then we're gonna use whatever we have at our disposal, which in this case happens to be the last project I was working on. Alright. So let's start the clock and discuss this feedback widget.\u003C/p>\u003Cp>Right? What are we actually trying to achieve with this thing? Let's discuss the functionality for this. Right? So functionality wise, basically, I want to track feedback on articles within our help center or documentation.\u003C/p>\u003Cp>And can we make this iframeable or, like, a custom element, I dare say? We'll probably start with iframe. Right. So we basically wanna manage our feedback on the articles within our help center or documentation. And to do that, obviously, we're gonna need a back end.\u003C/p>\u003Cp>Right? We can statically deploy a website. We could do all of that, but we need somewhere to store this feedback. Directus is gonna make that super simple for us. So what do we have for this specific one?\u003C/p>\u003Cp>Actually, let's let's talk about our data model before I dive into that. Right? Always helps to kinda map that out. So we already have an articles collection from the last episode. It has a title.\u003C/p>\u003Cp>Each article has a slug, and it has some content. We have categories for those articles as well. I'm not super worried about the categories. What we are going to add to the mix here is a feedback collection or ratings or what I I don't even know. You could call this a million things.\u003C/p>\u003Cp>Right? So we wanna give a an objective score, so an actual rating. We wanna have comments. Great. We need to track the article.\u003C/p>\u003Cp>Right? So we need to have a connection there. Basically, the nope. That's weird. Okay.\u003C/p>\u003Cp>Left is right and right is left. So we had the rating, we had the comments, we got the article. I think that's pretty much all we need or what we're looking for there. Could be wrong. We probably wanna track, like, the URL as well.\u003C/p>\u003Cp>Cool. So that's kinda what our data model looks like for this. It shouldn't be anything crazy. We could build, like, a dashboard out of this thing. You know, let's dive in.\u003C/p>\u003Cp>Right? So going back to the application, if we take a look, we've got this direct to since then it has categories. Great. Categories contain articles. Each article has a title, a slug, some content.\u003C/p>\u003Cp>You know, we can apply tags to it. We didn't really mess around with tags much on the last episode because never got there. Really struggled to adapt that template. But, hey, this is hundred apps, hundred hours. There's only so much you could do in an hour.\u003C/p>\u003Cp>And if it goes wrong, it goes wrong. Right? So what are we gonna do first? Let's start working on our back end. You know, you could see this amazing front end I've got here.\u003C/p>\u003Cp>Basically, this is our look. This is a really beautiful help center. If you don't agree with me, I'm sorry. But this is this thing is amazing. So we got an article called accessing items.\u003C/p>\u003Cp>There's a header for it. There's a title. Here's the markdown editor. We can actually see what that content looks like side by side. Ayo.\u003C/p>\u003Cp>It's a markdown editor. This is beautiful. Right? Hit save. Refresh.\u003C/p>\u003Cp>Boom. There it is. You know, I probably need to add some padding there. Let's just quickly do that. The designer OCD is kicking in.\u003C/p>\u003Cp>Let's just add a little bit of padding here. Of Of course, I'm a huge Tailwind guy. Tailwind nut. Okay. There we go.\u003C/p>\u003Cp>Great. Or we could even probably add a divider there. Get a little more separation. View divider or is it u? I'm using the Nuxt UI library.\u003C/p>\u003Cp>Yeah. There we go. Alright. So there we go. It's beautiful.\u003C/p>\u003Cp>What we wanna do is plop a big giant feedback widget down here at the bottom of this particular article. And I don't know why I took a screenshot of that, but that's what we're gonna do. First thing we wanna do though, at least on the back end, let's separate these tabs. We'll make this giant yeah, I could call this ratings. I'm inclined to just call it feedback.\u003C/p>\u003Cp>And, usually, I'm breaking my own convention here where I use, like, plurals for the table or collection names, but, hey, it's feedback. So the this was created on. We could call this the timestamp or, you know, created at. Let's say updated at. K.\u003C/p>\u003Cp>Status sort. We don't really need any of that. Do we really care the user that this was created by? You know, if you had users that were logged in on the Directus documentation, you know, maybe you do. Oh, I got those backwards created by created at or updated by.\u003C/p>\u003Cp>Gah. Updated by. Gosh. Alright. Not enough coffee this morning, actually.\u003C/p>\u003Cp>Alright. So we'll finish the setup of that one. We'll give this a rating, icon, rate. We give it a star, half star, save. Cool.\u003C/p>\u003Cp>Now, let's add a rating value. Right? We've got a rating. What do we want to give as far as our ratings? You know, do we wanna go three, four, five?\u003C/p>\u003Cp>I'm more inclined to do, like like, four as as far as the rating. Right? So let's set up a new input here. Maybe this is better served as something like a what do we have for this? We have a slider component.\u003C/p>\u003Cp>Slider. Slider. There we go. Alright. So the minimum value will say one, the maximum value is four, default value is null.\u003C/p>\u003Cp>Do we wanna always show the value? Yes. The step interval is one. We can open advanced field creation mode if we want. And then on the display side of it, we can visualize this as a number of stars.\u003C/p>\u003Cp>We could show it in a simple format, or we could just show the actual stars. So we'll do that. Why go with four? To me, I I hate giving people a middle ground. Right?\u003C/p>\u003Cp>So if we have bad or let's say, the worst, bad, and then good and great, there's no middle ground. Right? So you gotta get off the fence. We want to add some comments to this. So we'll use just like a text area.\u003C/p>\u003Cp>It's a great way to see that content, you know, maybe trim the start and end of that, and save. Right? Now what we need is let's just do the URL. We'll track that. I can add a little link input to the icon to the box, actually.\u003C/p>\u003Cp>And then we'll do what else are we gonna do here? We wanna do a relationship back to the article. Alright. So if we look at this feedback, we got like a rating system. Okay.\u003C/p>\u003Cp>We can have some comments here. There's a URL. But we probably wanna be able to, you know, see the feedback per article, instead of just by the URL. So what we're gonna do, we'll go to our feedback, and I could do this one of two ways. I can create the relationship inside the feedback, and that's a many to one relationship, or I can go into articles and create a one to many relationship there.\u003C/p>\u003Cp>You know, if you are creating the many to one and you do want that, inverse relationship, just make sure you check the boxes appropriately. I'm gonna go to articles. We're just gonna use the one to many relationship here. Let's call this feedback the feedback collection. The foreign key is gonna be article.\u003C/p>\u003Cp>So all the feedback, every single rating is gonna belong to an article, a single article. Like, we can't give the same feedback on multiple articles, basically. And we're gonna do the rating, and maybe we show, like, the comments as well for this. We show a link, and maybe this would be better as actually, like, a table as well. So we see the comments when this was created at and any comments.\u003C/p>\u003Cp>Okay. We will sort by the ratings. We could sort ascending. So, yes, let's show the worst ones first. Amazing.\u003C/p>\u003Cp>Alright. So now we've got feedback. If I go to the feedback collection, you could see we've got the article that it's related to there. And anytime you create that one to many relationship inside Directus, it's going to hide the corresponding mini to one relationship. So we're we're just going to, add that back.\u003C/p>\u003Cp>Great. And we probably wanna track, like, the URL here, like a fully qualified URL. This would be great for something like, hey, I've got development and I have production, where obviously you're gonna have different URLs for that. So you could filter that out even though it may be on like the same article locally. Or you could get around that a couple different ways by, you know, not even storing development feedback or, you know, some other type of system.\u003C/p>\u003Cp>But now if we go here, I should be able to go in and create feedback on this. You know, hey. This article is not beautiful, sir. And that is gonna be what? API HTTP local host 3,000 API slash accessing, is it two?\u003C/p>\u003Cp>Two c's? Accessing items. Right? Great. I've added some feedback.\u003C/p>\u003Cp>There it is. Boom. I can see that feedback now. I can see the URL. I can see the article.\u003C/p>\u003Cp>We got all of that. Great. But we don't want people to log in to Directus to actually interact with this feedback or leave us feedback. There's our ratings, the stars. Looks that looks really nice.\u003C/p>\u003Cp>Right? So what are we gonna do? We're gonna build that on the front end. Right? Cool.\u003C/p>\u003Cp>So if we are to interact with our API now, right, I could go in and basically, I could see the articles. I could see feedback and, you you know, I could do something like this in my params where I call fields and I do feedback.asterisk. So this first asterisk will give me all the root level fields. The second one will give me all the second level or the the root level fields for the feedback collection. If I actually save it there actually make that call, feedback.star.\u003C/p>\u003Cp>Great. So here I could see the actual ratings. Right? But if I were to actually try to post a rating or anything like that, at least publicly. Right?\u003C/p>\u003Cp>If I copy this same URL great. Open an incognito window. I am not even actually getting the info. Alright. I can't see the ratings.\u003C/p>\u003Cp>You know, previously, I had set up public permissions for the articles. I can't see the ratings for the feedback. And the reason for that is bada bing bada boom. Quack, quack, quack. Because we have, restrictive permissions, basically.\u003C/p>\u003Cp>Whenever you create new collections inside Directus, we don't add permissions for those by default. So we keep that data secure. So what we're gonna do, we'll go into our access policy here and, you know, this might not necessarily be what I would go to production with, but I'm gonna let people create feedback here and, you know, maybe I let them read the ID of the feedback. So as far as field permissions, I don't want them to be able to read our feedback. Maybe just get the ID after they've submitted that.\u003C/p>\u003Cp>Right? So we'll let them create feedback and then they can read the feedback of the ID they've created. Now a couple other ways we could do this, you know, if this was like a a static site, you could use like Vercel or Netlify functions that that you could potentially, you know, proxy to the direct us back end. You know, just a a a ton of different ways you could do this and you could have like a a separate role that controls the communication to the direct assistance. But for now, this will work.\u003C/p>\u003Cp>This will be fine. We're just gonna let anybody create that feedback. Right? So now let's start in on our feedback component. I'm just gonna create a new component.\u003C/p>\u003Cp>We'll call it feedback. Should we call it widget? Love widgets. Who doesn't love widgets? Alright.\u003C/p>\u003Cp>So I'm a script script tag first guy. Not sure if you are or not, but alright. So what do we actually need to define our you know, let's let's start with, like, props. Right? What what do we want for our props?\u003C/p>\u003Cp>The props are going to be define props. We do want an article ID. That's a that's a great prop to have. We do want to have the URL or should we we should be able to get that, actually. So I think all we actually need here as far as props is an article ID, and then we can get the current URL.\u003C/p>\u003Cp>This should be like use request URL from Nuxt. Use request URL. Let's just check their documentation. Use request URL. There's a helper function that returns the URL object working on both client side and user get request URL.\u003C/p>\u003Cp>Okay. So there we've got the request URL. Cool. And then, yeah, maybe we'll wanna define our ratings. Right?\u003C/p>\u003Cp>Const ratings, ratings map, or something like that. So we'll give a label. I I like using emojis for this. Let's do mad. Maybe we just use a trash can, like value trash.\u003C/p>\u003Cp>No. I'm using cursor here as the IDE. I I I like the auto completion for this, basically. Sometimes it's very finicky. Sometimes it's not.\u003C/p>\u003Cp>Yeah. It seems like we've come up with a good scale here. Cool. There's our ratings map. What else do we need here?\u003C/p>\u003Cp>We probably wanna track some comments. Comments equals ref string. No. That's just gonna be a string. We'll just, leave that as an empty string for now.\u003C/p>\u003Cp>And then let's go in and just like build a comments component. Right? And since that we're using cursor, let's just see what the sync can actually do. I'm just gonna hit command l and let's say build a feedback widget with the ratings map. Make it look beautiful.\u003C/p>\u003Cp>This is not how I've been prompting this thing, but build the, let's say, the template portion of the feedback widget with the ratings map. Make it look beautiful. Alright. So we can see here it's going through creating some code for us. I think it's using Claude three point five Sonnet here.\u003C/p>\u003Cp>There's our submit button. And I've got the Tailwind UI library here. So, you know, we could probably let's let's just see what it comes up with first. Feedback widget, app widget. Oh, no.\u003C/p>\u003Cp>That is not the right one. We want to what is going on here? Okay. Maybe we just copy paste this code here. It seems like I need to update the index for this or something like that.\u003C/p>\u003Cp>So we'll just copy paste. Great. There's our feedback widget. It's doing something. Obviously, styled with Tailwind.\u003C/p>\u003Cp>It's probably not gonna match what we've got, but we'll go to our slug. So here's our actual page. We've got some messy, messy stuff going on here. We are actually using the, Nuxt MDC library to just render this markdown content. And then we're gonna add a new separator.\u003C/p>\u003Cp>We'll add this feedback widget, article ID, this page dot ID. Did we even get the page dot ID? Yeah. We should have that page ID. Alright.\u003C/p>\u003Cp>So is this beautiful? Yes. Looks beautiful ish. Alright. We could do better.\u003C/p>\u003Cp>We could do better than this. Alright. Let's zoom out to, like, hundred percent. Yeah. I just kinda cut off here, the shadows, etcetera.\u003C/p>\u003Cp>We don't really want BG whites. And I think the Nuxt UI library has this uCard we could even drop this thing into. There we go. Let's remove all of this. K.\u003C/p>\u003Cp>How helpful was this article? Great. So now this is full width. Then we have new buttons. We can remove all of this.\u003C/p>\u003Cp>The title here, let's change that to label or no. We'll leave that. U buttons. What happens if we click this? Right?\u003C/p>\u003Cp>What? View button. Key equals rating value. We probably need a rating as well. Right?\u003C/p>\u003Cp>Rating. Ref number. We'll just leave that set to null to begin with. Number or null. K.\u003C/p>\u003Cp>And when we click that yeah. There you go. Cursor. We wanna set the rating equal to the rating dot value. Solid.\u003C/p>\u003Cp>There's the rating. Let's see what we get. Refresh. Something broken? Why are we not seeing anything good here?\u003C/p>\u003Cp>U buttons. Oh, yeah. U button. Dum dum. There we go.\u003C/p>\u003Cp>Alright. So now I could see there's our buttons. Those are not the great looking buttons that I want. So let's do, like, a ghost. Okay.\u003C/p>\u003Cp>There we go. And if I just wanted to show this, right, we could do rating. There's our rating. Is that actually gonna show the rating? Rating, rating, rating, rating.\u003C/p>\u003Cp>P p tag. Rating is null. Rating is we're using maybe conflicting rating value. There we go. Alright.\u003C/p>\u003Cp>So now we can see one, two, three, or four. Great. That's helpful. And then we have our extra comments. You know, let's make this giant text five XL.\u003C/p>\u003Cp>Great. We'll give these some padding too. Glass. P two. P four.\u003C/p>\u003Cp>Is this gonna give us some padding? Yeah. There we go. Cool. Alright.\u003C/p>\u003Cp>Looking great. This is trash. This is great. And let's do some additional cleanup here. So then we have a u form field, I think, is the name of this component, that wraps this, that provides us like a label.\u003C/p>\u003Cp>Additional comments. Okay. Looking great. And then we can do a u text area. V model, there's our comments, feedback comments.\u003C/p>\u003Cp>We don't need any of this. And then do we actually need we want this to be is block a prop here? It is not. So let's do width equals width full full width. Okay.\u003C/p>\u003Cp>Cool. Looking great. And then let's ditch this blue button. That'll be you button. Yeah.\u003C/p>\u003Cp>We can leave the type as submit. We'll just clear this out. Let's make that a block level button. Block and size equals x l. Make it a big button.\u003C/p>\u003Cp>Alright. And then in this case, I don't know why the overflow is being hidden here. Probably something to do with, like, one of my layouts. Overflow hidden. Yeah.\u003C/p>\u003Cp>There it is. Let's just solve for that. Right? Okay. So now we're good here.\u003C/p>\u003Cp>Let's add, like, a ring or something if this is selected. And let's do a little bit of cleanup. Right? So if there's no rating, the if if there's a rating value, we're gonna show the additional comments. So by default, this won't show and then it should expand.\u003C/p>\u003Cp>Now one of the other libraries that I love to use in my Nuxt projects is called form kit auto animate, auto animate dot form kit. It basically gives us, like, a nice animations and basically like a single line of code. Right? So you add this to your project. They've got a Nuxt module for this.\u003C/p>\u003Cp>And now if we go up to our card component, so that's where this is at, and I add this directive, v auto animate watch this. Watch this. What's v if if I got this inside a why is this not working as intended? Maybe because of the structure of the card component. And let's just wrap the inside of this in a div and see if that fixes it.\u003C/p>\u003Cp>Div. V auto animate. I don't wanna make a liar out of myself. Oh, there. Yeah.\u003C/p>\u003Cp>There we go. Now we got, like, some slick animations. Okay. V f rating dot value. You chose maybe we show, like, the yeah.\u003C/p>\u003Cp>You chose four out of four. You know, we could potentially even wrap these. And flex, justify between items baseline. Let's see what that looks like. It shows four out of four.\u003C/p>\u003Cp>Great. And then let's add, like, a conditional class. BG gray. No. I I wanna add a ring to this.\u003C/p>\u003Cp>Ring two, ring primary, 500. Right. There we go. We've got some additional comets. This is looking nice.\u003C/p>\u003Cp>Yeah. Solid. Cool. Looks good. We can submit feedback.\u003C/p>\u003Cp>If we actually we're not showing the additional comments, and then we submit the feedback. Right? Right now, this does absolutely nothing. So what we're gonna do, let's add a click. We're gonna submit the feedback, and then we'll go up and actually add a submit feedback function.\u003C/p>\u003Cp>Right? So we'll call this an async function because we're gonna be calling the directus API with this. We'll call it submit feedback. And, yeah, we don't really need to pass it any props. We can just pick those up from this widget.\u003C/p>\u003Cp>We got the current URL. Cool. Alright. So here's where we're gonna come back and use our direct us SDK. We're gonna do import create item from the Directus SDK.\u003C/p>\u003Cp>Great. And we're also going to pull in this Directus plugin from Nuxt app. So, you missed that part on the last episode where we set this up. Basically, I've got this simple Nuxt plug in. It pulls the Directus URL from the Nuxt runtime config, creates a rest client.\u003C/p>\u003Cp>There's a bunch of commented stuff here to deal with authentication that we're not handling, and then we provide that back to the directus application. So whenever the, like, the first visit occurs, like, this client gets created, and then we can use that same client throughout the entire Nuxt app here, simple integration with the SDK. Alright. So what we're gonna do, we'll wait for the response. You know, maybe we wanna show like a loading state as well.\u003C/p>\u003Cp>So we'll do loading ref equals false. We'll set loading dot value is equal to true. And then we can see that, like, direct us is our cursor here is trying to auto complete this, which I don't like. I don't like that auto complete. We will do this.\u003C/p>\u003Cp>So we're gonna call direct us dot request, and then we're gonna pass the method into here. So we're gonna create an item. We're gonna create an item in the feedback collection, and then we're gonna pass the body of that item. So we're gonna pass the URL, which is gonna be current URL. I don't know if that's gonna be value or, you know, let's make sure that's a string.\u003C/p>\u003Cp>Then we're gonna pass the rating, which is gonna be rating value dot value. And then we have comments dot value. And we could, you know, potentially wrap this and try catch, just so we could do error handling if we want to. Create item feedback. Yeah.\u003C/p>\u003Cp>Okay. Great. And then finally, set the loading value to false. Now I can also pass that loading prop here, And we could even use, like, the view shorthand syntax as well. Okay.\u003C/p>\u003Cp>So, let's say disabled unless there's a rating value. If there's no rating value, then this is disabled. Right? So if we haven't picked anything, we can't click the button. Cool.\u003C/p>\u003Cp>Alright. So now if we click the button, we could do that. We can add some comments. If I watch the network request tab here, we submit that. We see some data coming back.\u003C/p>\u003Cp>We got the ID. There's the payload. Then we can go in and actually check and see whether we got our feedback or not. Cool. Alright.\u003C/p>\u003Cp>So what did we miss? We got the URL. We got the rating. The rating sucks. We didn't add any comments.\u003C/p>\u003Cp>Okay? We forgot to pass the article. Right? So the article equals the props. Article ID, and that should be required.\u003C/p>\u003Cp>Cool. Let's try this one more time. Right? So this is an amazing article. This is amazing, Bryant.\u003C/p>\u003Cp>Alright. So there we go. We can see how quick that is. Obviously, that's local. There's our feedback we submitted.\u003C/p>\u003Cp>Let's go back to our feedback collection. This is amazing. Bryant, here's our comments. Here's our rating. This looks great.\u003C/p>\u003Cp>Alright? There is our feedback widget. We could do a lot to improve this, obviously. But what if we wanted to, like, maybe embed this? Right?\u003C/p>\u003Cp>Could we iframe this? You know, Vue is a, like, a good framework for web components. I've not really dove into a lot of, like, the custom elements and, like, web component stuff with Vue. I don't really wanna tackle it on this. That could probably be its own episode.\u003C/p>\u003Cp>But what if we put this feedback widget on its own routes and we just try to embed it into other pages? So let's do this. We will create a new route. Let's call it feedback.view. Cool.\u003C/p>\u003Cp>We'll initiate, like, the base setup here. And you think I would set up, like, my snippets correctly at this point, but I have not. And then what are we gonna do? We're gonna use the routes, and we're gonna get the article ID. Actually, is it like a use query or we could just call query from this, but I don't think Nuxt provides like a use query.\u003C/p>\u003Cp>Let's call the article ID equals route.query. Article ID. K. And then we're gonna just drop this feedback widget in here. It's almost like talking out loud.\u003C/p>\u003Cp>This thing knows what I wanna do. And then we should probably pass the blank layout for this because otherwise, if I go here and I go to feedback, we're gonna get the rest of this, like my header and everything else. Right? So then I could go in here and const, no. It's actually define page meta meta layout equals blank.\u003C/p>\u003Cp>Does that give us what we want? Cool. Awesome. Alright. Now let's make sure v f article ID to v else, no article ID provided.\u003C/p>\u003Cp>So there's no article ID. And then if we do something like this, article ID equals test. Okay. So now we got that. Maybe we want to pass the current URL.\u003C/p>\u003Cp>Can an iframe get the current URL? Great question for, great question for AI, I think. Can the can an iframe get the URL of the host? Using JavaScript only works with the same origin policy blah blah blah. Yeah.\u003C/p>\u003Cp>So maybe we wanna pass, like, the URL. Cool. Alright. And then that means we may need to adjust our feedback widget just a little bit. So feedback widget.\u003C/p>\u003Cp>We can add a URL prop. This could be an optional prop. That'll be a string. And equals URL or props dot URL if that exists or we're gonna use the request URL. Cool.\u003C/p>\u003Cp>Cool. Great. Yeah. So now if I try this, this should throw an error coming back from Directus because I'm not using a proper ID. Right?\u003C/p>\u003Cp>Unexpected error occurred, and that's because the article ID that I passed here is not correct. Right, because there is no existing article ID there. So we could, you know, do something like making that optional or making that null if there is no article ID, I guess. But, you know, that's not really what we're after here. We just wanna stick this in an iframe.\u003C/p>\u003Cp>Alright. Cool. So now let's just open up a new can we do, like, HTML? Yeah. There we go.\u003C/p>\u003Cp>Div id app h one. Here's a you know what? Why am I not just relying on AI? Create a sample web page about hundred apps, hundred hours. Not a a Vue app, just a standard HTML five page.\u003C/p>\u003Cp>And it is using Tailwind as well, of course. Yes. We'll just copy this guy. There we go. Alright.\u003C/p>\u003Cp>This is a weather dashboard. We're just gonna drop this on my desktop, index. HTML, along for the days of index HTML sometimes. Alright. And then how do we get this to open up?\u003C/p>\u003Cp>Here's our weather dashboard. Yeah. Who knows? This this is amazing. Now, right, what if we try to add this iframe?\u003C/p>\u003Cp>Iframe local host feedback article URL ID blah blah blah. Is this actually gonna work? Okay. That's great. We've got, like, this.\u003C/p>\u003Cp>I wish probably do we wanna set a height for this? Do we need to set a height? Height equals, I don't know, 300 px. Okay. So the styling is not necessarily what we need out of this.\u003C/p>\u003Cp>MTE four. Refresh. Okay. Cool. In the feedback widget, let's say BG transparent.\u003C/p>\u003Cp>Where is that coming from? Where is the white coming from? Where are you coming from, friend? Is that just coming from, like, app dot view or something? No.\u003C/p>\u003Cp>The default layout blank. Style dot, let's just do HTML and body, background color equals transparent. There we go. Okay. That gets what we want for the blank.\u003C/p>\u003Cp>Now this is looking good. And we can actually embed this. Right? Now, let's set the height to maybe 600 pixels. Give this enough room to expand.\u003C/p>\u003Cp>Okay. So now we've got this iframed in here. Will this actually work when we submit feedback? Feedback local host errors are expected. Right?\u003C/p>\u003Cp>Because we don't have the proper ID, it's not sending that to direct us correctly. So, you know, maybe we update this widget. Blah blah blah. Undefined props. Article ID or undefined.\u003C/p>\u003Cp>Feedback. What are we trying to send? Article is one. Okay. So we are sending an article there.\u003C/p>\u003Cp>So if we go into our index.HTML, maybe we just remove this. K. We pass our URL. Page not found. Feedback.\u003C/p>\u003Cp>Oh, there you go. No article ID provided. Feedback. Let's just remove that piece. We don't really need that.\u003C/p>\u003Cp>Okay. Cool. There it is. There's our widget. We can submit feedback on this.\u003C/p>\u003Cp>Cool. There it is. Is this coming through okay? Local host. Yeah.\u003C/p>\u003Cp>So there we go. We've got our URL there. Cool. What's what's next? Right?\u003C/p>\u003Cp>We need, like, a success state for this thing, and then we could probably call this one gravy. Right? Let's add just like a success state. Success ref equals false. And then success equals true.\u003C/p>\u003Cp>And then we go down here and, all of our submit stuff. Right? We're just gonna wrap this in a template. V if no success, v else. Thanks for your feedback.\u003C/p>\u003Cp>And you can see why I like cursor. Like, once you get enough of your logic in there, it it does start to, you know, be very helpful as far as auto completion. This is a pretty cool episode. Right? Quack.\u003C/p>\u003Cp>Quack. Quack. There we go. We'll hit submit. There you go.\u003C/p>\u003Cp>Thanks for your feedback. We're good. You know, we could get even more sophisticated with this as I roll away. We could, you know, potentially, like, add a cookie or something to track whether they submitted this. And if they've already submitted feedback on the article, not let them submit again.\u003C/p>\u003Cp>I don't think that's really necessary. Right? Let's use the remaining time that we have to just, like, quickly build, like, a dashboard to see this feedback. So we'll go in, let's call this our feedback dashboard or help center dashboard. Cool.\u003C/p>\u003Cp>We'll add insights for this. So we'll just create a new panel for it. Let's show just a list of, like, the total feedback. Right? The field is gonna be ID.\u003C/p>\u003Cp>We'll count these. What is the filter gonna be? Do we actually need a filter? No. We don't.\u003C/p>\u003Cp>We'll just say total feedback submissions. Great. And boom. Now we get a metric panel that shows us the total number of feedback submissions. Let's add a new one, where we track, like, the average rating.\u003C/p>\u003Cp>Right? So I could do this in a metric. Let's see if we can get this meter component, what this will actually show, what it'll look like. So we got the primary field here is gonna be rating or the the field that we're summarizing. We want to average that.\u003C/p>\u003Cp>And the max is gonna be four. And there's our color, Filter, Stroke, Medium, Conditional Styles. Yeah. If the is less than or equal to two is red. Let's do, like, yellow for less than or equal to three.\u003C/p>\u003Cp>And then, otherwise, maybe this is green? I'm not sure what this is gonna show. Average rating, 60%. Okay. So this is showing, like, a percentage of this.\u003C/p>\u003Cp>Right? And that's conditional. So if I go in and I were to I'm not saying you should do this ever, and you'd probably wanna lock down the permissions for this to make it noneditable after the fact. Alright. I go into our help center.\u003C/p>\u003Cp>Now we see 85. So I'm guessing the value here Where's that coming in at? Oh, this should be green. Right. Cool.\u003C/p>\u003Cp>So, you know, 85%. Probably, the meter is not the best function for that. We should just use the average here. Sort field. We don't really need a sort field for that.\u003C/p>\u003Cp>Maximum decimals. Let's go to, like, one decimal. And average rating. Let's see what that comes up of. There we go.\u003C/p>\u003Cp>So average rating is 3.4. I can open this up over here. Or can I? Let's do this. There we go.\u003C/p>\u003Cp>And then I can blast some feedback. Test. Oh, yeah. You need to actually do an article, though, don't you, Brian? API slash accessing items.\u003C/p>\u003Cp>Alright. So now we could blast some feedback on this. This sucks. Boom. Just refresh.\u003C/p>\u003Cp>I don't suggest spamming your own help center. But then we could see that score change, obviously, because there are eight total submissions for this thing. Let's keep building out. Right? Maybe we wanna see a list of the individual articles and their feedback.\u003C/p>\u003Cp>Can we do that? Like a list of, here's the feedback. And we give a list. We want to show the URL. We wanna show the rating.\u003C/p>\u003Cp>And we want to, like, comments are gonna get a bit tricky there, but we can sort by when they were created at. Makes sense to me. We'll sort by descending, so we'll see the new ones first. List of feedback. There it is.\u003C/p>\u003Cp>Cool. Again, that designer OCD is kicking in, so let's make this same width. And one of the the finer points about the dashboards here is when you slide them together, the border radius, snaps. So here we can see all the feedback. Great.\u003C/p>\u003Cp>I can open this up and see the actual ratings. Let's go in and just solve that. That's going to aggravate me. Like, what I can do here is, I can disable editing for this. So this will control editing from the Directus Studio here.\u003C/p>\u003Cp>Now if I wanted to control, like, whether you can update this or not via the API, that would be in the permission settings. You'd probably wanna set that up to where, like, certain team members cannot edit this value. But, now you can see, like, all this is locked down when I open it up inside the studio. And, you know, if I open it here through the dashboard, it's not gonna show either. So it's a great way to do that.\u003C/p>\u003Cp>One of the last things that I just wanna showcase here is the ability to add, like, a global variables and global relational variables. So let's just create a global variable called time or date from, and let's make this a time stamp. We'll use, like, a default value of, I don't know, November 1. Daytime, 24 value. DateFrom.\u003C/p>\u003Cp>Okay. So we define a key here. Right? This is cool. Define the key.\u003C/p>\u003Cp>And obviously I created all these today so I'm not sure how impressive this is going to be. But what I could do here is when I set up like this list of feedback, I could go in and define my filters. So I wanna do createdAt is greater than or is in between, let's say, date from, and let's do dollar sign now. Cool. Dollar sign now.\u003C/p>\u003Cp>Okay. If we do November 27, December third as the date from, you know, if I go here, you could see that that is filtered out. And, likewise, I can apply the same filter. Copy raw value. Paste raw value.\u003C/p>\u003Cp>Paste raw value. Paste raw value. Paste raw value. And you can see this is just the, these are the standard filter rules inside Directus. You could find all those in the documentation.\u003C/p>\u003Cp>And we go back through November. Right. I see a list of all the items, and, obviously, that updates all the different metrics. Right? The other cool thing that I can do is add a relational.\u003C/p>\u003Cp>I was trying to combine relational and variable there. So let's call this the the actual article. Right? So I pick an article and let's call this the, let's use the title of the article in our display template for this. Pick an Article.\u003C/p>\u003Cp>K. Cool. And this will give us a drop down where we can pick an article. Cool. Great.\u003C/p>\u003Cp>There's accessing items. Doesn't do anything though. Right? So we're gonna do the same thing where we add a filter for this. So the article, the should be like article ID equals article.\u003C/p>\u003Cp>So because we've defined that that variable here, this is accessible anywhere on this dashboard. Alright. And as soon as I start to add that you can see Article that it adjusts the values that we're seeing. Article ID equals Article. Awesome.\u003C/p>\u003Cp>So now I'm only seeing the data that is in this date range and through this specific article. So if I pick WIP, there's nothing there for that one because that one is not published. And now I could see, boom, we have the feedback for our specific article. That's it. This is our amazing feedback widget episode on 100 apps, one hundred hours.\u003C/p>\u003Cp>What's more to say? I think this is a a great tutorial, something that, you guys can take away and, could be helpful in a lot of different contexts. So let's call that a win. Eight minutes and thirty seconds left on the clock. Roll the beautiful bean footage as they say.\u003C/p>\u003Cp>We'll catch you on the next episode of 100 apps, one hundred hours. We'll see you.\u003C/p>","And we're back with another episode of 100 apps, one hundred hours. I'm your host Brian Gillespie here for Directus. And today, we are building a feedback widget. And you know what they say, if it quacks like a duck, it's probably a duck. So we'll see what the duck is going on today. I've got this amazing hockey jersey that, my brother-in-law gifted me a couple years ago. Rocking that today. So what are we rocking on the build today? We're rocking a feedback widget and I don't often, like, carry over builds from episode to episode. But if you watched the last episode, you probably watched me fail to try and massage a help center template into something that pulls the data remotely, pulls the content remotely from Directus as a CMS instead of local markdown files. Rectified that issue a little bit off scene. And today, we're gonna carry that same theme. We're gonna be build a feedback widget for the documentation site. So if this is your first go with hundred apps hundred hours, you might wanna go back and watch me fail miserably at that one. Maybe not. You'll still get value out of this one. You'll be able to follow along. You don't have to watch that one. But the rules are, we have sixty minutes to plan, sixty minutes to build. Well, it's just sixty minutes total. Plan and build. No more, no less. And then we're gonna use whatever we have at our disposal, which in this case happens to be the last project I was working on. Alright. So let's start the clock and discuss this feedback widget. Right? What are we actually trying to achieve with this thing? Let's discuss the functionality for this. Right? So functionality wise, basically, I want to track feedback on articles within our help center or documentation. And can we make this iframeable or, like, a custom element, I dare say? We'll probably start with iframe. Right. So we basically wanna manage our feedback on the articles within our help center or documentation. And to do that, obviously, we're gonna need a back end. Right? We can statically deploy a website. We could do all of that, but we need somewhere to store this feedback. Directus is gonna make that super simple for us. So what do we have for this specific one? Actually, let's let's talk about our data model before I dive into that. Right? Always helps to kinda map that out. So we already have an articles collection from the last episode. It has a title. Each article has a slug, and it has some content. We have categories for those articles as well. I'm not super worried about the categories. What we are going to add to the mix here is a feedback collection or ratings or what I I don't even know. You could call this a million things. Right? So we wanna give a an objective score, so an actual rating. We wanna have comments. Great. We need to track the article. Right? So we need to have a connection there. Basically, the nope. That's weird. Okay. Left is right and right is left. So we had the rating, we had the comments, we got the article. I think that's pretty much all we need or what we're looking for there. Could be wrong. We probably wanna track, like, the URL as well. Cool. So that's kinda what our data model looks like for this. It shouldn't be anything crazy. We could build, like, a dashboard out of this thing. You know, let's dive in. Right? So going back to the application, if we take a look, we've got this direct to since then it has categories. Great. Categories contain articles. Each article has a title, a slug, some content. You know, we can apply tags to it. We didn't really mess around with tags much on the last episode because never got there. Really struggled to adapt that template. But, hey, this is hundred apps, hundred hours. There's only so much you could do in an hour. And if it goes wrong, it goes wrong. Right? So what are we gonna do first? Let's start working on our back end. You know, you could see this amazing front end I've got here. Basically, this is our look. This is a really beautiful help center. If you don't agree with me, I'm sorry. But this is this thing is amazing. So we got an article called accessing items. There's a header for it. There's a title. Here's the markdown editor. We can actually see what that content looks like side by side. Ayo. It's a markdown editor. This is beautiful. Right? Hit save. Refresh. Boom. There it is. You know, I probably need to add some padding there. Let's just quickly do that. The designer OCD is kicking in. Let's just add a little bit of padding here. Of Of course, I'm a huge Tailwind guy. Tailwind nut. Okay. There we go. Great. Or we could even probably add a divider there. Get a little more separation. View divider or is it u? I'm using the Nuxt UI library. Yeah. There we go. Alright. So there we go. It's beautiful. What we wanna do is plop a big giant feedback widget down here at the bottom of this particular article. And I don't know why I took a screenshot of that, but that's what we're gonna do. First thing we wanna do though, at least on the back end, let's separate these tabs. We'll make this giant yeah, I could call this ratings. I'm inclined to just call it feedback. And, usually, I'm breaking my own convention here where I use, like, plurals for the table or collection names, but, hey, it's feedback. So the this was created on. We could call this the timestamp or, you know, created at. Let's say updated at. K. Status sort. We don't really need any of that. Do we really care the user that this was created by? You know, if you had users that were logged in on the Directus documentation, you know, maybe you do. Oh, I got those backwards created by created at or updated by. Gah. Updated by. Gosh. Alright. Not enough coffee this morning, actually. Alright. So we'll finish the setup of that one. We'll give this a rating, icon, rate. We give it a star, half star, save. Cool. Now, let's add a rating value. Right? We've got a rating. What do we want to give as far as our ratings? You know, do we wanna go three, four, five? I'm more inclined to do, like like, four as as far as the rating. Right? So let's set up a new input here. Maybe this is better served as something like a what do we have for this? We have a slider component. Slider. Slider. There we go. Alright. So the minimum value will say one, the maximum value is four, default value is null. Do we wanna always show the value? Yes. The step interval is one. We can open advanced field creation mode if we want. And then on the display side of it, we can visualize this as a number of stars. We could show it in a simple format, or we could just show the actual stars. So we'll do that. Why go with four? To me, I I hate giving people a middle ground. Right? So if we have bad or let's say, the worst, bad, and then good and great, there's no middle ground. Right? So you gotta get off the fence. We want to add some comments to this. So we'll use just like a text area. It's a great way to see that content, you know, maybe trim the start and end of that, and save. Right? Now what we need is let's just do the URL. We'll track that. I can add a little link input to the icon to the box, actually. And then we'll do what else are we gonna do here? We wanna do a relationship back to the article. Alright. So if we look at this feedback, we got like a rating system. Okay. We can have some comments here. There's a URL. But we probably wanna be able to, you know, see the feedback per article, instead of just by the URL. So what we're gonna do, we'll go to our feedback, and I could do this one of two ways. I can create the relationship inside the feedback, and that's a many to one relationship, or I can go into articles and create a one to many relationship there. You know, if you are creating the many to one and you do want that, inverse relationship, just make sure you check the boxes appropriately. I'm gonna go to articles. We're just gonna use the one to many relationship here. Let's call this feedback the feedback collection. The foreign key is gonna be article. So all the feedback, every single rating is gonna belong to an article, a single article. Like, we can't give the same feedback on multiple articles, basically. And we're gonna do the rating, and maybe we show, like, the comments as well for this. We show a link, and maybe this would be better as actually, like, a table as well. So we see the comments when this was created at and any comments. Okay. We will sort by the ratings. We could sort ascending. So, yes, let's show the worst ones first. Amazing. Alright. So now we've got feedback. If I go to the feedback collection, you could see we've got the article that it's related to there. And anytime you create that one to many relationship inside Directus, it's going to hide the corresponding mini to one relationship. So we're we're just going to, add that back. Great. And we probably wanna track, like, the URL here, like a fully qualified URL. This would be great for something like, hey, I've got development and I have production, where obviously you're gonna have different URLs for that. So you could filter that out even though it may be on like the same article locally. Or you could get around that a couple different ways by, you know, not even storing development feedback or, you know, some other type of system. But now if we go here, I should be able to go in and create feedback on this. You know, hey. This article is not beautiful, sir. And that is gonna be what? API HTTP local host 3,000 API slash accessing, is it two? Two c's? Accessing items. Right? Great. I've added some feedback. There it is. Boom. I can see that feedback now. I can see the URL. I can see the article. We got all of that. Great. But we don't want people to log in to Directus to actually interact with this feedback or leave us feedback. There's our ratings, the stars. Looks that looks really nice. Right? So what are we gonna do? We're gonna build that on the front end. Right? Cool. So if we are to interact with our API now, right, I could go in and basically, I could see the articles. I could see feedback and, you you know, I could do something like this in my params where I call fields and I do feedback.asterisk. So this first asterisk will give me all the root level fields. The second one will give me all the second level or the the root level fields for the feedback collection. If I actually save it there actually make that call, feedback.star. Great. So here I could see the actual ratings. Right? But if I were to actually try to post a rating or anything like that, at least publicly. Right? If I copy this same URL great. Open an incognito window. I am not even actually getting the info. Alright. I can't see the ratings. You know, previously, I had set up public permissions for the articles. I can't see the ratings for the feedback. And the reason for that is bada bing bada boom. Quack, quack, quack. Because we have, restrictive permissions, basically. Whenever you create new collections inside Directus, we don't add permissions for those by default. So we keep that data secure. So what we're gonna do, we'll go into our access policy here and, you know, this might not necessarily be what I would go to production with, but I'm gonna let people create feedback here and, you know, maybe I let them read the ID of the feedback. So as far as field permissions, I don't want them to be able to read our feedback. Maybe just get the ID after they've submitted that. Right? So we'll let them create feedback and then they can read the feedback of the ID they've created. Now a couple other ways we could do this, you know, if this was like a a static site, you could use like Vercel or Netlify functions that that you could potentially, you know, proxy to the direct us back end. You know, just a a a ton of different ways you could do this and you could have like a a separate role that controls the communication to the direct assistance. But for now, this will work. This will be fine. We're just gonna let anybody create that feedback. Right? So now let's start in on our feedback component. I'm just gonna create a new component. We'll call it feedback. Should we call it widget? Love widgets. Who doesn't love widgets? Alright. So I'm a script script tag first guy. Not sure if you are or not, but alright. So what do we actually need to define our you know, let's let's start with, like, props. Right? What what do we want for our props? The props are going to be define props. We do want an article ID. That's a that's a great prop to have. We do want to have the URL or should we we should be able to get that, actually. So I think all we actually need here as far as props is an article ID, and then we can get the current URL. This should be like use request URL from Nuxt. Use request URL. Let's just check their documentation. Use request URL. There's a helper function that returns the URL object working on both client side and user get request URL. Okay. So there we've got the request URL. Cool. And then, yeah, maybe we'll wanna define our ratings. Right? Const ratings, ratings map, or something like that. So we'll give a label. I I like using emojis for this. Let's do mad. Maybe we just use a trash can, like value trash. No. I'm using cursor here as the IDE. I I I like the auto completion for this, basically. Sometimes it's very finicky. Sometimes it's not. Yeah. It seems like we've come up with a good scale here. Cool. There's our ratings map. What else do we need here? We probably wanna track some comments. Comments equals ref string. No. That's just gonna be a string. We'll just, leave that as an empty string for now. And then let's go in and just like build a comments component. Right? And since that we're using cursor, let's just see what the sync can actually do. I'm just gonna hit command l and let's say build a feedback widget with the ratings map. Make it look beautiful. This is not how I've been prompting this thing, but build the, let's say, the template portion of the feedback widget with the ratings map. Make it look beautiful. Alright. So we can see here it's going through creating some code for us. I think it's using Claude three point five Sonnet here. There's our submit button. And I've got the Tailwind UI library here. So, you know, we could probably let's let's just see what it comes up with first. Feedback widget, app widget. Oh, no. That is not the right one. We want to what is going on here? Okay. Maybe we just copy paste this code here. It seems like I need to update the index for this or something like that. So we'll just copy paste. Great. There's our feedback widget. It's doing something. Obviously, styled with Tailwind. It's probably not gonna match what we've got, but we'll go to our slug. So here's our actual page. We've got some messy, messy stuff going on here. We are actually using the, Nuxt MDC library to just render this markdown content. And then we're gonna add a new separator. We'll add this feedback widget, article ID, this page dot ID. Did we even get the page dot ID? Yeah. We should have that page ID. Alright. So is this beautiful? Yes. Looks beautiful ish. Alright. We could do better. We could do better than this. Alright. Let's zoom out to, like, hundred percent. Yeah. I just kinda cut off here, the shadows, etcetera. We don't really want BG whites. And I think the Nuxt UI library has this uCard we could even drop this thing into. There we go. Let's remove all of this. K. How helpful was this article? Great. So now this is full width. Then we have new buttons. We can remove all of this. The title here, let's change that to label or no. We'll leave that. U buttons. What happens if we click this? Right? What? View button. Key equals rating value. We probably need a rating as well. Right? Rating. Ref number. We'll just leave that set to null to begin with. Number or null. K. And when we click that yeah. There you go. Cursor. We wanna set the rating equal to the rating dot value. Solid. There's the rating. Let's see what we get. Refresh. Something broken? Why are we not seeing anything good here? U buttons. Oh, yeah. U button. Dum dum. There we go. Alright. So now I could see there's our buttons. Those are not the great looking buttons that I want. So let's do, like, a ghost. Okay. There we go. And if I just wanted to show this, right, we could do rating. There's our rating. Is that actually gonna show the rating? Rating, rating, rating, rating. P p tag. Rating is null. Rating is we're using maybe conflicting rating value. There we go. Alright. So now we can see one, two, three, or four. Great. That's helpful. And then we have our extra comments. You know, let's make this giant text five XL. Great. We'll give these some padding too. Glass. P two. P four. Is this gonna give us some padding? Yeah. There we go. Cool. Alright. Looking great. This is trash. This is great. And let's do some additional cleanup here. So then we have a u form field, I think, is the name of this component, that wraps this, that provides us like a label. Additional comments. Okay. Looking great. And then we can do a u text area. V model, there's our comments, feedback comments. We don't need any of this. And then do we actually need we want this to be is block a prop here? It is not. So let's do width equals width full full width. Okay. Cool. Looking great. And then let's ditch this blue button. That'll be you button. Yeah. We can leave the type as submit. We'll just clear this out. Let's make that a block level button. Block and size equals x l. Make it a big button. Alright. And then in this case, I don't know why the overflow is being hidden here. Probably something to do with, like, one of my layouts. Overflow hidden. Yeah. There it is. Let's just solve for that. Right? Okay. So now we're good here. Let's add, like, a ring or something if this is selected. And let's do a little bit of cleanup. Right? So if there's no rating, the if if there's a rating value, we're gonna show the additional comments. So by default, this won't show and then it should expand. Now one of the other libraries that I love to use in my Nuxt projects is called form kit auto animate, auto animate dot form kit. It basically gives us, like, a nice animations and basically like a single line of code. Right? So you add this to your project. They've got a Nuxt module for this. And now if we go up to our card component, so that's where this is at, and I add this directive, v auto animate watch this. Watch this. What's v if if I got this inside a why is this not working as intended? Maybe because of the structure of the card component. And let's just wrap the inside of this in a div and see if that fixes it. Div. V auto animate. I don't wanna make a liar out of myself. Oh, there. Yeah. There we go. Now we got, like, some slick animations. Okay. V f rating dot value. You chose maybe we show, like, the yeah. You chose four out of four. You know, we could potentially even wrap these. And flex, justify between items baseline. Let's see what that looks like. It shows four out of four. Great. And then let's add, like, a conditional class. BG gray. No. I I wanna add a ring to this. Ring two, ring primary, 500. Right. There we go. We've got some additional comets. This is looking nice. Yeah. Solid. Cool. Looks good. We can submit feedback. If we actually we're not showing the additional comments, and then we submit the feedback. Right? Right now, this does absolutely nothing. So what we're gonna do, let's add a click. We're gonna submit the feedback, and then we'll go up and actually add a submit feedback function. Right? So we'll call this an async function because we're gonna be calling the directus API with this. We'll call it submit feedback. And, yeah, we don't really need to pass it any props. We can just pick those up from this widget. We got the current URL. Cool. Alright. So here's where we're gonna come back and use our direct us SDK. We're gonna do import create item from the Directus SDK. Great. And we're also going to pull in this Directus plugin from Nuxt app. So, you missed that part on the last episode where we set this up. Basically, I've got this simple Nuxt plug in. It pulls the Directus URL from the Nuxt runtime config, creates a rest client. There's a bunch of commented stuff here to deal with authentication that we're not handling, and then we provide that back to the directus application. So whenever the, like, the first visit occurs, like, this client gets created, and then we can use that same client throughout the entire Nuxt app here, simple integration with the SDK. Alright. So what we're gonna do, we'll wait for the response. You know, maybe we wanna show like a loading state as well. So we'll do loading ref equals false. We'll set loading dot value is equal to true. And then we can see that, like, direct us is our cursor here is trying to auto complete this, which I don't like. I don't like that auto complete. We will do this. So we're gonna call direct us dot request, and then we're gonna pass the method into here. So we're gonna create an item. We're gonna create an item in the feedback collection, and then we're gonna pass the body of that item. So we're gonna pass the URL, which is gonna be current URL. I don't know if that's gonna be value or, you know, let's make sure that's a string. Then we're gonna pass the rating, which is gonna be rating value dot value. And then we have comments dot value. And we could, you know, potentially wrap this and try catch, just so we could do error handling if we want to. Create item feedback. Yeah. Okay. Great. And then finally, set the loading value to false. Now I can also pass that loading prop here, And we could even use, like, the view shorthand syntax as well. Okay. So, let's say disabled unless there's a rating value. If there's no rating value, then this is disabled. Right? So if we haven't picked anything, we can't click the button. Cool. Alright. So now if we click the button, we could do that. We can add some comments. If I watch the network request tab here, we submit that. We see some data coming back. We got the ID. There's the payload. Then we can go in and actually check and see whether we got our feedback or not. Cool. Alright. So what did we miss? We got the URL. We got the rating. The rating sucks. We didn't add any comments. Okay? We forgot to pass the article. Right? So the article equals the props. Article ID, and that should be required. Cool. Let's try this one more time. Right? So this is an amazing article. This is amazing, Bryant. Alright. So there we go. We can see how quick that is. Obviously, that's local. There's our feedback we submitted. Let's go back to our feedback collection. This is amazing. Bryant, here's our comments. Here's our rating. This looks great. Alright? There is our feedback widget. We could do a lot to improve this, obviously. But what if we wanted to, like, maybe embed this? Right? Could we iframe this? You know, Vue is a, like, a good framework for web components. I've not really dove into a lot of, like, the custom elements and, like, web component stuff with Vue. I don't really wanna tackle it on this. That could probably be its own episode. But what if we put this feedback widget on its own routes and we just try to embed it into other pages? So let's do this. We will create a new route. Let's call it feedback.view. Cool. We'll initiate, like, the base setup here. And you think I would set up, like, my snippets correctly at this point, but I have not. And then what are we gonna do? We're gonna use the routes, and we're gonna get the article ID. Actually, is it like a use query or we could just call query from this, but I don't think Nuxt provides like a use query. Let's call the article ID equals route.query. Article ID. K. And then we're gonna just drop this feedback widget in here. It's almost like talking out loud. This thing knows what I wanna do. And then we should probably pass the blank layout for this because otherwise, if I go here and I go to feedback, we're gonna get the rest of this, like my header and everything else. Right? So then I could go in here and const, no. It's actually define page meta meta layout equals blank. Does that give us what we want? Cool. Awesome. Alright. Now let's make sure v f article ID to v else, no article ID provided. So there's no article ID. And then if we do something like this, article ID equals test. Okay. So now we got that. Maybe we want to pass the current URL. Can an iframe get the current URL? Great question for, great question for AI, I think. Can the can an iframe get the URL of the host? Using JavaScript only works with the same origin policy blah blah blah. Yeah. So maybe we wanna pass, like, the URL. Cool. Alright. And then that means we may need to adjust our feedback widget just a little bit. So feedback widget. We can add a URL prop. This could be an optional prop. That'll be a string. And equals URL or props dot URL if that exists or we're gonna use the request URL. Cool. Cool. Great. Yeah. So now if I try this, this should throw an error coming back from Directus because I'm not using a proper ID. Right? Unexpected error occurred, and that's because the article ID that I passed here is not correct. Right, because there is no existing article ID there. So we could, you know, do something like making that optional or making that null if there is no article ID, I guess. But, you know, that's not really what we're after here. We just wanna stick this in an iframe. Alright. Cool. So now let's just open up a new can we do, like, HTML? Yeah. There we go. Div id app h one. Here's a you know what? Why am I not just relying on AI? Create a sample web page about hundred apps, hundred hours. Not a a Vue app, just a standard HTML five page. And it is using Tailwind as well, of course. Yes. We'll just copy this guy. There we go. Alright. This is a weather dashboard. We're just gonna drop this on my desktop, index. HTML, along for the days of index HTML sometimes. Alright. And then how do we get this to open up? Here's our weather dashboard. Yeah. Who knows? This this is amazing. Now, right, what if we try to add this iframe? Iframe local host feedback article URL ID blah blah blah. Is this actually gonna work? Okay. That's great. We've got, like, this. I wish probably do we wanna set a height for this? Do we need to set a height? Height equals, I don't know, 300 px. Okay. So the styling is not necessarily what we need out of this. MTE four. Refresh. Okay. Cool. In the feedback widget, let's say BG transparent. Where is that coming from? Where is the white coming from? Where are you coming from, friend? Is that just coming from, like, app dot view or something? No. The default layout blank. Style dot, let's just do HTML and body, background color equals transparent. There we go. Okay. That gets what we want for the blank. Now this is looking good. And we can actually embed this. Right? Now, let's set the height to maybe 600 pixels. Give this enough room to expand. Okay. So now we've got this iframed in here. Will this actually work when we submit feedback? Feedback local host errors are expected. Right? Because we don't have the proper ID, it's not sending that to direct us correctly. So, you know, maybe we update this widget. Blah blah blah. Undefined props. Article ID or undefined. Feedback. What are we trying to send? Article is one. Okay. So we are sending an article there. So if we go into our index.HTML, maybe we just remove this. K. We pass our URL. Page not found. Feedback. Oh, there you go. No article ID provided. Feedback. Let's just remove that piece. We don't really need that. Okay. Cool. There it is. There's our widget. We can submit feedback on this. Cool. There it is. Is this coming through okay? Local host. Yeah. So there we go. We've got our URL there. Cool. What's what's next? Right? We need, like, a success state for this thing, and then we could probably call this one gravy. Right? Let's add just like a success state. Success ref equals false. And then success equals true. And then we go down here and, all of our submit stuff. Right? We're just gonna wrap this in a template. V if no success, v else. Thanks for your feedback. And you can see why I like cursor. Like, once you get enough of your logic in there, it it does start to, you know, be very helpful as far as auto completion. This is a pretty cool episode. Right? Quack. Quack. Quack. There we go. We'll hit submit. There you go. Thanks for your feedback. We're good. You know, we could get even more sophisticated with this as I roll away. We could, you know, potentially, like, add a cookie or something to track whether they submitted this. And if they've already submitted feedback on the article, not let them submit again. I don't think that's really necessary. Right? Let's use the remaining time that we have to just, like, quickly build, like, a dashboard to see this feedback. So we'll go in, let's call this our feedback dashboard or help center dashboard. Cool. We'll add insights for this. So we'll just create a new panel for it. Let's show just a list of, like, the total feedback. Right? The field is gonna be ID. We'll count these. What is the filter gonna be? Do we actually need a filter? No. We don't. We'll just say total feedback submissions. Great. And boom. Now we get a metric panel that shows us the total number of feedback submissions. Let's add a new one, where we track, like, the average rating. Right? So I could do this in a metric. Let's see if we can get this meter component, what this will actually show, what it'll look like. So we got the primary field here is gonna be rating or the the field that we're summarizing. We want to average that. And the max is gonna be four. And there's our color, Filter, Stroke, Medium, Conditional Styles. Yeah. If the is less than or equal to two is red. Let's do, like, yellow for less than or equal to three. And then, otherwise, maybe this is green? I'm not sure what this is gonna show. Average rating, 60%. Okay. So this is showing, like, a percentage of this. Right? And that's conditional. So if I go in and I were to I'm not saying you should do this ever, and you'd probably wanna lock down the permissions for this to make it noneditable after the fact. Alright. I go into our help center. Now we see 85. So I'm guessing the value here Where's that coming in at? Oh, this should be green. Right. Cool. So, you know, 85%. Probably, the meter is not the best function for that. We should just use the average here. Sort field. We don't really need a sort field for that. Maximum decimals. Let's go to, like, one decimal. And average rating. Let's see what that comes up of. There we go. So average rating is 3.4. I can open this up over here. Or can I? Let's do this. There we go. And then I can blast some feedback. Test. Oh, yeah. You need to actually do an article, though, don't you, Brian? API slash accessing items. Alright. So now we could blast some feedback on this. This sucks. Boom. Just refresh. I don't suggest spamming your own help center. But then we could see that score change, obviously, because there are eight total submissions for this thing. Let's keep building out. Right? Maybe we wanna see a list of the individual articles and their feedback. Can we do that? Like a list of, here's the feedback. And we give a list. We want to show the URL. We wanna show the rating. And we want to, like, comments are gonna get a bit tricky there, but we can sort by when they were created at. Makes sense to me. We'll sort by descending, so we'll see the new ones first. List of feedback. There it is. Cool. Again, that designer OCD is kicking in, so let's make this same width. And one of the the finer points about the dashboards here is when you slide them together, the border radius, snaps. So here we can see all the feedback. Great. I can open this up and see the actual ratings. Let's go in and just solve that. That's going to aggravate me. Like, what I can do here is, I can disable editing for this. So this will control editing from the Directus Studio here. Now if I wanted to control, like, whether you can update this or not via the API, that would be in the permission settings. You'd probably wanna set that up to where, like, certain team members cannot edit this value. But, now you can see, like, all this is locked down when I open it up inside the studio. And, you know, if I open it here through the dashboard, it's not gonna show either. So it's a great way to do that. One of the last things that I just wanna showcase here is the ability to add, like, a global variables and global relational variables. So let's just create a global variable called time or date from, and let's make this a time stamp. We'll use, like, a default value of, I don't know, November 1. Daytime, 24 value. DateFrom. Okay. So we define a key here. Right? This is cool. Define the key. And obviously I created all these today so I'm not sure how impressive this is going to be. But what I could do here is when I set up like this list of feedback, I could go in and define my filters. So I wanna do createdAt is greater than or is in between, let's say, date from, and let's do dollar sign now. Cool. Dollar sign now. Okay. If we do November 27, December third as the date from, you know, if I go here, you could see that that is filtered out. And, likewise, I can apply the same filter. Copy raw value. Paste raw value. Paste raw value. Paste raw value. Paste raw value. And you can see this is just the, these are the standard filter rules inside Directus. You could find all those in the documentation. And we go back through November. Right. I see a list of all the items, and, obviously, that updates all the different metrics. Right? The other cool thing that I can do is add a relational. I was trying to combine relational and variable there. So let's call this the the actual article. Right? So I pick an article and let's call this the, let's use the title of the article in our display template for this. Pick an Article. K. Cool. And this will give us a drop down where we can pick an article. Cool. Great. There's accessing items. Doesn't do anything though. Right? So we're gonna do the same thing where we add a filter for this. So the article, the should be like article ID equals article. So because we've defined that that variable here, this is accessible anywhere on this dashboard. Alright. And as soon as I start to add that you can see Article that it adjusts the values that we're seeing. Article ID equals Article. Awesome. So now I'm only seeing the data that is in this date range and through this specific article. So if I pick WIP, there's nothing there for that one because that one is not published. And now I could see, boom, we have the feedback for our specific article. That's it. This is our amazing feedback widget episode on 100 apps, one hundred hours. What's more to say? I think this is a a great tutorial, something that, you guys can take away and, could be helpful in a lot of different contexts. So let's call that a win. Eight minutes and thirty seconds left on the clock. Roll the beautiful bean footage as they say. We'll catch you on the next episode of 100 apps, one hundred hours. We'll see you.","9c2ff38d-5ab4-4e8b-955a-a083c80f88e4",[620],"2ba92584-506d-452f-8b3b-ac72790ab0cb",[],{"id":142,"number":143,"show":122,"year":144,"episodes":623},[146,147,148,149,150,151,152,153,154,155],{"id":151,"slug":625,"vimeo_id":626,"description":627,"tile":628,"length":285,"resources":8,"people":8,"episode_number":288,"published":556,"title":629,"video_transcript_html":630,"video_transcript_text":631,"content":8,"seo":632,"status":130,"episode_people":633,"recommendations":635,"season":636},"internal-knowledge-base","1059436262","Bryant builds a Guru-inspired knowledge base with bite-sized information cards accessible through a Chrome extension. Watch as he creates a Directus backend with verification flows, then builds a browser extension interface that makes company knowledge searchable and editable on the fly—despite a tricky bug that nearly derails the entire project.","900c0ff2-b753-449c-bc21-59ec9cc3f342","Mission: Internal Knowledgebase","\u003Cp>Speaker 0: And we are back with another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. Welcome back. I'm here for Directus, by Directus something something. I I don't know.\u003C/p>\u003Cp>I'm trying to be fancy here. Not my forte, obviously. Dad of three little girls, gave up being cool a long time ago. But today, we have got a cool episode for you. We are going to be tackling an internal knowledge base.\u003C/p>\u003Cp>I'll explain what that is in a moment, but if you're new to 100 apps, one hundred hours, there are two basic rules. Number one, we have sixty minutes to plan and build your app, your clone, your idea, whatever this is. You get no more, no less. And rule number two, the anti rule. The no holds barred WrestleMania action, use whatever you have at your disposal.\u003C/p>\u003Cp>This is a no holds barred cage match, basically. So let's talk about internal knowledge base for a moment. When I used to run a customer success team, at a previous company, there was this product called Guru that was basically an internal knowledge base. You know, we used it for a lot of our chat support team. Also our CSMs, you know, if they had a question, we could easily document this.\u003C/p>\u003Cp>And the cool thing that they did have was this Chrome extension that would basically give you access to all the knowledge base. And, you know, the structure was very bite sized pieces of information, and that was the whole idea behind it. So we're gonna try and replicate something similar. You know, anytime you have something like an internal knowledge base, if it's not in everyone's face, the knowledge goes stale. If it's a pain in the butt to edit, it goes stale.\u003C/p>\u003Cp>So let's tackle that challenge. We're gonna put sixty minutes on the clock. Let's let's ride. Let's wrassle. Alright.\u003C/p>\u003Cp>So, functionality for this specific app. Right? And this is me probably biting off a lot more than I can actually chew in sixty minutes. But what we're gonna do, we're gonna build a back end for the internal knowledge base, And then we're going to build a Chrome extension to search search articles, edit articles, and that's probably the extent of maybe what we'll get done in an hour. I don't even know if we'll get this done.\u003C/p>\u003Cp>Right? But, we need to kinda flush this out further. Let's go in and map some data model. Map this out. Right?\u003C/p>\u003Cp>So at a high level, we're we're gonna have users of this application, so we'll have some authentication, etcetera. You know, they'll be able to log in. As far as the actual knowledge itself, I like the term cards for this. Instead of articles, that's what we're gonna do. And the reason why is these are bite sized pieces of internal knowledge.\u003C/p>\u003Cp>I think that's a helpful paradigm. As far as the fields that we want, we're gonna have a title for the card. Maybe I won't even get into, like, AI generated summary of this. We may use some AI stuff to, like, dive into this. Let's have an owner of this card.\u003C/p>\u003Cp>Like, who owns this knowledge internally? We've got the actual content of the card for sure. You know, maybe we got some tags for the cards as well. And then, of course, we have tags. Let's say there's a title for the tag and then a topic or an area.\u003C/p>\u003Cp>Right? I think that's a good structure for tags. Like, in in the case of Directus, that may be, like, features or frameworks or something like that. And then we'll have users. So and a user owns a card.\u003C/p>\u003Cp>Tags are connected to cards in some form or fashion shape, form, or fashion to quote the West Virginia governor here. This is a many to many relationship between cards and tags. Right? A card could have multiple tags. A tag could have multiple cards or a single tag could be applied to multiple cards.\u003C/p>\u003Cp>So that is the data model setup we are going to roll with. You know, if you've watched this before, I've got, like, a standard Nuxt starter that I use for this. But, on the front end, we're gonna try this Chrome extension Vite starter from, mister Fayezara. Fayez Fayez Ahmed. I think that's his name.\u003C/p>\u003Cp>Yep. Fayez. Hopefully, I'm pronouncing that right. If I'm not, I apologize. But, let's focus on that back end first.\u003C/p>\u003Cp>Right? We are going to zoom way in and let's create our first collection. Right? Let's call this cards, and I'm gonna give these a generated UUID. We'll say when the card was created, who created it, when it was updated.\u003C/p>\u003Cp>What's the status of the card? You know, this could be helpful if we have, like, cards that are draft or not approved or not verified, I would say. That should be like the one of the first states that we have for this. And then we'll add a a sort order, I guess. Why not?\u003C/p>\u003Cp>Let's see if we could find a card. So this is a looks like a memory card, a SIM card. It's a good icon for this. Great. I'm just gonna hit command s to save this and stay.\u003C/p>\u003Cp>And let's start with the title. So straight input field string. We do wanna require a value for this. Right? Great.\u003C/p>\u003Cp>You know, we could potentially index this field if we want to inside the database. That's what that's for. Do we need to trim? We don't need this to be URL safe, so we're gonna roll with that. And then what else do we have?\u003C/p>\u003Cp>We had an some content for this. Let's go with the WYSIWYG editor. We'll have some content, you know, and for, like, AI heavy stuff, we might even, like, set this to be markdown. Basically, behind the scenes, these use the same type for the column inside the database. And what Directus is doing behind the scenes whenever I add this field is basically just creating a new column inside the cards table inside our Postgres database.\u003C/p>\u003Cp>So for now, let's, let's go to the markdown interface just to appease our developer friends. And let's say we'll auto format the display for this. Cool. Great. Gravy.\u003C/p>\u003Cp>Amazing. What else do we need? Right? We wanna assign ownership of this card. And, you know, maybe we even want to have, like, a date verified.\u003C/p>\u003Cp>So let's take care of the ownership first. Right? I'm gonna add a many to one relationship here. So I'm gonna just call this the owner of this card, the related collection. Whenever you spin up a Directus instance, it gives us, some system collections that it prefixes with Directus so that all of your your SQL stays pure, which is nice.\u003C/p>\u003Cp>And then we're just going to add the Directus users collection here. So this is what the instance uses for authentication, you know, into the studio and via the API. So we'll just piggyback on that. Right? We'll assign a user to this.\u003C/p>\u003Cp>And for the display, cool, great, everything looks nice. We'll display should be an option to display the user. We could show the user in a circle. That will look great. And let's move that owner beside this.\u003C/p>\u003Cp>We want to show the status. Date updated. So there's the status title owner. And maybe we want to switch that around. Basically, what I'm doing here is just controlling the form for anybody using the back end of this.\u003C/p>\u003Cp>So let's make the title full width just to give that proper weight, proper gravitas. Here's the status. Maybe we wanna update that from draft to you know, it's not necessarily like we're concerned about whether this is published or not. We just want it to be verified. So, like, internally, you wanna make sure that that content is totally verified.\u003C/p>\u003Cp>So that's what we'll do. We'll call it verified. This is archived. Cool. We could copy these choices and just paste those into our labels.\u003C/p>\u003Cp>And I don't wanna show as a dot. I just wanna show each actual label. Alright. So next, maybe we add a time field, date time. Let's do this as time stamps.\u003C/p>\u003Cp>We'll just follow the same convention, date verified. Cool. Awesome. Alright. And we'll come back to that in a moment.\u003C/p>\u003Cp>This is our cards collection. Right? Let's call this direct us features. I'm gonna assign this to me, the admin user, and we will here's all the amazing features of Directus. Great.\u003C/p>\u003Cp>REST APIs, GraphQL APIs, APIS APIs. There we go. Insights, permissions, whatever. Right? Great.\u003C/p>\u003Cp>There's our preview for the markdown. You know, if we decide to do some AI, you should later this is gonna be a a friendlier format for that. Great. We've got a card. Cool.\u003C/p>\u003Cp>We can move this around. You know, let's add just a a hook to this using some flows to automatically populate the date verified. Right? So, anytime I verify a card, let's say a card is updated, updated or created, card is updated, created. You know, we could be add verification stamp.\u003C/p>\u003Cp>Alright. And basically, anytime a card is created or, you know, let's just say updated in this point because the default should be like draft, the state for all of the cards. Anytime a card is updated, let's go in and let's make sure check the status. Check status. Alright.\u003C/p>\u003Cp>We're just gonna create a condition here and this uses the standard, filter rules inside Directus. What I'm gonna do is just copy our template here. We're gonna adjust this a little bit. Payload dot status. We'll drill in one more layer And if the status is equal to verified, we're gonna run some action after this, right.\u003C/p>\u003Cp>So inside Flows is a great way to add simple automations to your back end And there is flow control already built into flows. Shocker. Right? But if this condition passes, we're going to take action on it. If it doesn't pass, meaning the status is not updated or it's not there, it will, you know, follow the rejection path.\u003C/p>\u003Cp>So in this case, you know, I I could log something to the console. In this case, I'm just going to, run a little script. Get current time. Right. We're gonna return, what's new date?\u003C/p>\u003Cp>Is that gonna be two ISO? Is it two ISO string like that? New date to ISO string. Is that what we're looking for? New date to string to ISO string.\u003C/p>\u003Cp>That should be what I think that's what we're looking for. So we're just gonna return the current time. Great. And then I'm going to update the data. Update card.\u003C/p>\u003Cp>Cool. So we'll do cards. We can use the permissions from the trigger. And I'm gonna say this should be dollar sign trigger dot keys, the first item in that array. Not a % sure, but, date verified.\u003C/p>\u003Cp>And then we'll do something like this. We'll just use this mustache syntax, git underscore current time. Great. Cool. Let's see if this actually works.\u003C/p>\u003Cp>Alright. So this is good. These are all amazing features of Directus. I'm gonna verify this. I hit save.\u003C/p>\u003Cp>And voila. Now we could see the exact date and time that I verified this thing, December 3 '10 '14 AM. Amazing. Sweet. Right?\u003C/p>\u003Cp>Alright. Let's go back and continue our March onto our data model here. So let's set up some tags. I'm not really concerned about when these were updated, etcetera, blah blah blah. You know, maybe we wanna sort the tags, but, let's find a tag.\u003C/p>\u003Cp>We could use the hashtag. That's what tags are for. This is a title. Cool. Title of the tag.\u003C/p>\u003Cp>That's required. And we'll keep keep updating. We don't wanna change just yet, my friend. And then the topic. Right?\u003C/p>\u003Cp>What are we gonna call this? Area topic? I think I like topic for this. I still wanna keep the string. I don't think there's a need for extra topics, but what we'll do, we'll just pick the options from a drop down.\u003C/p>\u003Cp>Right? So in the case of Directus, you know, this could be features features of Directus. Makes sense. Frameworks, like next Nuxt, etcetera. Frameworks.\u003C/p>\u003Cp>Can we get another f one? What else is this gonna be? API, API, pricing, you know, the pricing of direct you know, these could be infinite, and maybe that's, you know, not not a great one because that could be just like a a side effect. Cool. So now we have tags.\u003C/p>\u003Cp>We want to create this relationship between the two, and it doesn't really matter where I start here. All roads lead to the many to many relationship. If I'm on the cards collection, I'm gonna create a new field. We're gonna call it keys or I'm sorry. Call the key is gonna be tags.\u003C/p>\u003Cp>I need more coffee today. The related collection is gonna be tags. We don't wanna allow duplicate tags. We wanna show a link to this item. But what I wanna do here, I'm gonna open up the advanced field creation mode, and you could see what's going on behind the scenes.\u003C/p>\u003Cp>Directus is creating a junction collection for us. I'm gonna change the names of these just for my own anxiety, I guess. So we're gonna call this card tags. So these are tags on a card. The junction field or the the foreign key is card for the card, tag for the tag, and then we're gonna add this corresponding field to tags as well.\u003C/p>\u003Cp>We're gonna call those cards, and then the sort is gonna be sort. I'll just set these to delete the junction table item in case we deselect a tag from any of these. There's no reason to keep, the extra info in this junction collection. Great. We'll display some related values and away we go.\u003C/p>\u003Cp>Right? Now I'm gonna set like a default display template here just in case we're using that in some of the views. We'll do the same for the tags. We'll give that a title. Maybe we wanna show the actual topic.\u003C/p>\u003Cp>Cool. Topic. Great. Boom. Boom.\u003C/p>\u003Cp>Boom. Boom. Boom. We've got all the relationships. We've got everything configured.\u003C/p>\u003Cp>This is a feature tag. We don't have a tag for features. Well, we have a topic for features. What is this gonna be? This is, REST APIs.\u003C/p>\u003Cp>Cool. Directus REST API. It's awesome. Cool. Alright.\u003C/p>\u003Cp>So there we go. Now we have some tags. We have title. We have some content. We have the tags here.\u003C/p>\u003Cp>I can see all the cards within that. This is looking freaking sweet as far as the back end. Like, if I want to fetch all my cards, I could just open up the direct assistance. I'll swap the admin side of this. I'll go to items cards.\u003C/p>\u003Cp>Here I can see all of that. And one of my favorite features of the REST API is the ability to do stuff like this where I can say, hey, I want all the root level fields. That's what this wild card is for. And then I also want all the fields for the tags. So you could see here, this is I'm actually going through the junction collection.\u003C/p>\u003Cp>So I probably wanna go one step further and do this. So I can actually see, okay. Here's my tags. Here's the tag. What's the title of the tag?\u003C/p>\u003Cp>What's the topic? Etcetera. And I, you know, that's not really recommended for production use. Right? I would probably wanna be more specific so that you don't end up with, like, six levels of recursion because you could see the cards start to show here.\u003C/p>\u003Cp>Tags dot topic. Cool. There we go. There's the tag. There's the title.\u003C/p>\u003Cp>Topic. You know, I could go in and keep going with this. You get the idea. You get the picture. Alright.\u003C/p>\u003Cp>Let's bring that back. So I've still got access to my direct as admin. And then we are going to fire this thing up. Is this gonna go well? I do not know.\u003C/p>\u003Cp>So we're gonna pull this up, and I'm probably better off, like, starting a new directory for this. CD sites. Cool. We're gonna, like, what? Git clone?\u003C/p>\u003Cp>Can I page? Git clone. The extension. Okay. CD, the extension, and code dot.\u003C/p>\u003Cp>Is that the, that's not gonna open it in cursor. We're gonna cheat. Is it really cheating? I don't think so. We're gonna use cursor today.\u003C/p>\u003Cp>Alright. So looks like what do we have? We got PMPMI, kinda your standard. So we'll wait for this terminal to pop up. PMPMI.\u003C/p>\u003Cp>Okay. Come on. Come on. Core pack. Do I have to enable core pack?\u003C/p>\u003Cp>Core pack enable? Core pack enabler. That's what I am. User local core pack target in this. What is going on here?\u003C/p>\u003Cp>CD my extension, cloned to local. PMPMI. What is going on here? Unexpected identifier core pack. Is this just like a bad terminal instance?\u003C/p>\u003Cp>PPMI. Yeah. Wild. Okay. I guess the shell wasn't loaded yet before I fired this thing up.\u003C/p>\u003Cp>Alright. So now we'll do p m p m dev. Never used this thing before, so it should be fun watching me sweat. What are we gonna do here? Then load the extension in the browser within the extension folder.\u003C/p>\u003Cp>Okay. So, I am using Arc. I'm gonna switch over to Chrome for this. I'm gonna drag this out of the way. You know what?\u003C/p>\u003Cp>I I do like Arc. There are some frustrations with Arc, but, you know, it is what it is. Let's go to where is it? Slash extensions? Extensions?\u003C/p>\u003Cp>We'll just manage our extensions here. Let's load and unpack extension. You can see I've got, like, Vue already installed on this one. Extension select. Failed to load extension.\u003C/p>\u003Cp>What are my docs saying? Extension extension dev, I guess. Let's try the dev folder. Okay. We got v extension.\u003C/p>\u003Cp>Cool. What is this gonna do? Can we pin to the toolbar? Tim the toolbar, Taylor. Okay.\u003C/p>\u003Cp>Cool. So now we have this cool little extension. Alright. Dark mode, airplane mode, VPN. I don't know what any of these settings are doing, but, this looks cool.\u003C/p>\u003Cp>This is our little pop up widget. Right? I'm assuming somewhere in the source folder here we can find what we need for this. Alright. So this is kinda structured.\u003C/p>\u003Cp>I I've worked on Chrome extensions a little bit in the past. Let's see. We've got the pages. We got an app. So we're showing a router view, which is nice.\u003C/p>\u003Cp>So we can navigate between these specific things. We've got pages, index.HTML. We have the module. That's the specific pop up. You know, we've got, like, content scripts and things like that.\u003C/p>\u003Cp>I'm assuming if I open up this, this will say hello from content script. Let's actually for now, let's just focus on the pop up side of things. Right? I don't so much care about the settings here or the about. Where's the about page?\u003C/p>\u003Cp>That's the profile page, I'm assuming. Profile page. Put your own content here. Cool. Alright.\u003C/p>\u003Cp>How are we gonna get started with this thing? Let's see if we can actually just fetch a list of our articles, and we're gonna take over the home page here. The first thing I'm gonna do is move this script setup to the top. We'll do lang t s typescripts. Great.\u003C/p>\u003Cp>This has already got Tailwind installed, so I'm cruising there. Let's call this the we'll give this a little header actions. Is it articles, cards? Directus knowledge cards. That sounds great.\u003C/p>\u003Cp>Knowledge cards. Okay. Div. Great. Yeah.\u003C/p>\u003Cp>We'll do flex call. Okay. I'm I'm gonna roll with you there. Now as far as the components, let's not over engineer first. Okay.\u003C/p>\u003Cp>So now we get into, like, our first kinda inflection point here. We want to PMPM install the Directus SDK. Awesome. We'll put the SDK in here. We'll import where am I gonna stick the SDK clients?\u003C/p>\u003Cp>Util services, you know, we could call it that. Services. We will create a directus.ts file. We're gonna import the rest. We're gonna create Directus from the Directus SDK.\u003C/p>\u003Cp>Read items, read item, update item. I'm just gonna import the whole house here. Right? Create an item, delete an item. And one of the nice things about the Directus SDK is how modular it is.\u003C/p>\u003Cp>Right? So I can only I only have to import the items that I actually need. And I'm just gonna hard code the app URL here. That's our Directus URL, 8055. Create Directus.\u003C/p>\u003Cp>Okay. And then we can export Directus. Do we want that to be you know, let's call it export cons. So I could still, you know, access Directus. But also let's, let's carry this further and just get like a a function.\u003C/p>\u003Cp>Export async function. Get cards. Get cards. Cool. Yeah.\u003C/p>\u003Cp>Maybe we wanna potentially pass like a like params for that. We're gonna commit typescript no nos here. We'll just slap any on there. And what we're gonna do here, we'll say cards. We're going to what?\u003C/p>\u003Cp>Await. Directus. That's not that's not a good auto completion cursor. Alright. So we're gonna say read items, knowledge cards, params.\u003C/p>\u003Cp>We're gonna return the cards. Get card. Yeah. There we go. Okay.\u003C/p>\u003Cp>Now it's starting to get the picture. Right? This is one of the reasons why I have adopted cursor is because once it zeros in on, like, the logic for how you do some of this stuff, it is pretty good about actually taking care of it for you. Right? It's not gonna be knowledge cards though.\u003C/p>\u003Cp>And that could be solved by actually giving it, you know, like a TypeScript definition of what our types are, which you could potentially grab from the Directus open API spec. Alright. So let's go back to the pop up. Now we've got the service. We're going to import get cards from at services.\u003C/p>\u003Cp>I hope that is the actual alias for this. We'll we're about to find out, honestly. Do we wanna do cards? Why would we do cards as a reference? Cards equals await get cards.\u003C/p>\u003Cp>And are we gonna pass any params? Let's just get all the fields for the cards. It failed. Services from Directus. Yeah.\u003C/p>\u003Cp>I kinda figured that was gonna be the case. Okay. Now we've got that. Let's just go in. Oh, we've got this.\u003C/p>\u003Cp>We could format this a little bit. One of the first things I like to do just to make sure I'm getting the data that I want is just output this thing. So we'll save. Refresh. Can I actually see the network requests that are occurring through this thing?\u003C/p>\u003Cp>Inspect? What does that do? Does that give me okay. So now we're here inside the oh, as soon as you throw the pop up away, it disappears. Inspect.\u003C/p>\u003Cp>Is there a way to save this? The pop up is gonna disappear on me. So if I go to here so wait. Get cards. Are we actually finding that?\u003C/p>\u003Cp>Pages pop up. Dot.services.directus.js. Is that where how we should import this thing? I don't see nothing. What's happening?\u003C/p>\u003Cp>Maybe we should actually wrap this in, try catch so we could see some errors. Right? Try return cars. We'll just log the errors out. And now if we pop open the console, right, should we see some errors happening?\u003C/p>\u003Cp>Console failed to load. This is forbidden forbidden. We can't access this. Why is that happening? I kinda thought this would be the case.\u003C/p>\u003Cp>Right? So by default, whenever I create new collections, Directus does not provide any access to those collections. Right? So if I look at the Public Access Policy, I got nothing coming from that. We got thirty minutes on the clock, so for now let's, let's just grant full access to this.\u003C/p>\u003Cp>We are working locally. You know, if I was going to basically go to production with this, obviously, I would implement authentication. Maybe we can circle back to that. But, you know, the core functionality here is being able to update, edit, and rearrange these cards. Alright.\u003C/p>\u003Cp>So now with those permissions populated, we save this again. Can we actually see what's going on here? Console component, setup component. I don't see, get cards. Is there a way to actually sticky the inspection window?\u003C/p>\u003Cp>How do I do that? Focus page. I thought there was a way to to keep this up regardless, but maybe not for the pop up window. Settings, type error, node. Let's just try to restart.\u003C/p>\u003Cp>Let's see what we got. Do we have to reload this extension every single time? Refresh? I sure hope not, but we will if we have to. Now I don't see any.\u003C/p>\u003Cp>So we should be able to, like, console log the cards. Here's the cards. Right? Should be able to see some kind of output from that. Inspect.\u003C/p>\u003Cp>Console. Scheduler flush view internals. It's not liking that for whatever reason. Right? Maybe we want to do on on mounted.\u003C/p>\u003Cp>Cause cards. Response. Cards dot value equals response. Cards dot value.\u003C/p>\u003Cp>Speaker 1: Hey. Okay. Yeah.\u003C/p>\u003Cp>Speaker 0: It doesn't like fetching when the component is created for whatever reason. Not sure why that is, but there we go. We've got our cards. We could see the cards here. Cool.\u003C/p>\u003Cp>I can also do what we talked about where I can say, okay. I wanna get the tag. And where I'm using the SDK, I can also do something like this where we have tags, and then I can provide, like, an object like structure. So within that, we have, tag, and then we have the title. Let's fetch the ID of the tag and then the maybe even the topic.\u003C/p>\u003Cp>Tags. Tag. Okay. So now we could see all the info we want. That's working correctly.\u003C/p>\u003Cp>Let's, let's actually render out these cards. Right? So we'll do, like, a div. V four in cars key card dot ID. What are you gonna come up with cursor?\u003C/p>\u003Cp>Title.subscription. No. There's not gonna be a description for end. Maybe we show the tags within that. So span b four tag in card dot tags tags.\u003C/p>\u003Cp>Cool. Rest knowledge card, v API. Class. Let's give them some padding. We'll add a light border, round, large, rounded, l g.\u003C/p>\u003Cp>See what that looks like. Okay. And this is actually gonna be is this gonna be router link? Router link is gonna be missing two, a car dot ID. Okay.\u003C/p>\u003Cp>And then we're gonna need pages. So what does our actual router look like? Right? Okay. Looks like cursor is doing its thing for me here.\u003C/p>\u003Cp>So the path dot card dot ID pages card. So we're gonna create a new card page. Cool. Card dot view. I probably need to import that as well.\u003C/p>\u003Cp>Card view. Oh, it is dynamically importing those. Okay. That's fine. We can stick with that.\u003C/p>\u003Cp>Vbase TS setup. Move my script up to the top. And then we can, like, nab this card thing as well. But for now, this should solve the challenge. Okay.\u003C/p>\u003Cp>So now we can see the card. There's a direct us REST API. Card is looking nice. Let's go back to this. Maybe we give the title a little styling.\u003C/p>\u003Cp>Text large, font bold. Okay. We add a little margin to the top of this, give it a border, give a little padding to the top. Okay. So now we got our cards.\u003C/p>\u003Cp>We're probably gonna want a, like, a search input as well. Search input. If I do something like this, what is cursor gonna do for me? Input, placeholder, search. We could search our cards.\u003C/p>\u003Cp>Cool. And then, you know, we might even wanna do something like this where we have a query, and then we're gonna add, like, a watch to this. Watch search new val. What are we gonna do as far as the fetching? It's actually await get cards unmounted.\u003C/p>\u003Cp>And what we could do on the new value, we are going to if new value, cars have we don't wanna search cars that way. We could just use the directus API for this. So let's set up the watch, just to make sure that watcher is actually working here. And I could at least do this on the Directus documentation so we got something nice to kinda look at. Right?\u003C/p>\u003Cp>If I change the search here, it should be updating watch search. It's not console logging that new value for the search. Why is that? Because I don't have a v model on the search input. Let's try this again.\u003C/p>\u003Cp>Console. Why is the search are we outputting the sir okay. So now we can start seeing that. Great. Does this thing have something like view use already plugged in for, like, a debounce?\u003C/p>\u003Cp>I don't see that. But alright. We're we're not gonna worry too much about the debounce right now, and we're just gonna ignore this. Like, let's get to the actual card view for now. And, god, I really wanna change that router, that footer.\u003C/p>\u003Cp>We'll just drop the footer off of this. Cool. Alright. So now we can see the cards. Here's the settings.\u003C/p>\u003Cp>Cool. Great. How much time are we cruising out? We got twenty two minutes left. Let's see if we can get to actually editing these cards.\u003C/p>\u003Cp>So, if we take some of our logic here from the actual home page, we can probably just reuse a lot of this. We could get card, card. We don't need a search. Get card. And, again, I'm not totally sure why this thing is not wanting to card dot value equals response.\u003C/p>\u003Cp>Why it's not wanting to fetch properly? You know, if we're using script setup, it should do that for us, but, I digress. It's at a h one. Text two XL border b. P b four.\u003C/p>\u003Cp>Cool. And does it sound like tailwind typography in it? I don't know. We're just gonna do div class. Text large.\u003C/p>\u003Cp>Oh, we can add a prose class in there just to see. And one of the main reasons I love Tailwind is just the ability to, like, quickly do stuff like this. Alright. Git card. We're gonna git card.\u003C/p>\u003Cp>That is in the services.directus.js. Git card. Right? Search is not defined. Why do I need search?\u003C/p>\u003Cp>Oh, because we're watching that value. We don't need to watch that business. Alright. Go to home. So refresh this.\u003C/p>\u003Cp>We go to the direct API. Why is the ID is not defined. Oh, okay. Yep. We need to pass the ID.\u003C/p>\u003Cp>So that's gonna be what? Const route equals use route params dot ID. Are we passing that? Yep. We do have that.\u003C/p>\u003Cp>Cool. So now we should be getting that ID.\u003C/p>\u003Cp>Speaker 1: What is the oh, we\u003C/p>\u003Cp>Speaker 0: had to import that. Forgot I'm not working in Nuxt. Nuxt does all the fancy auto imports for us. So we refresh. There it is.\u003C/p>\u003Cp>Directus REST API. We can go home. We can go back to that card. Cool. Alright.\u003C/p>\u003Cp>Let's do const is editing. Ref equals false. Does this have auto imports? I'm assuming it does because it is. Let's see.\u003C/p>\u003Cp>Auto import, components auto importing, feed plugin components. We'll just import ref to be safe. Right? Obviously, that's already working, but let's give this some padding as well, make this look a little nicer, and we'll add a little margin to the top of this bad boy. Okay.\u003C/p>\u003Cp>Alright. So now we have our content. We could view this. Let's add a button to edit this thing. Right?\u003C/p>\u003Cp>Flex items center gap for I'll just say justify between, and then we're gonna add a button. Okay. We can edit this card. That's cool. The border b will move here.\u003C/p>\u003Cp>Cool. Alright. And if we edit this, what are we gonna use for the editor? View markdown editor. View markdown editor.\u003C/p>\u003Cp>What do we have here? What do we have? This is, again, risky business. Markdown editor in v three. Oh, no.\u003C/p>\u003Cp>Alright. This is it's not markdown. Base editor. Where are the demos? Import?\u003C/p>\u003Cp>Base editor? Oh, I guess we're this just doesn't have, like, some nice syntax. Right? Code mirror editor? Yeah.\u003C/p>\u003Cp>This looks way is this too cumbersome for what we're looking for? I probably should have snuffed this out earlier. Right? View editor, MD editor for view. This one's got 1,300 stars.\u003C/p>\u003Cp>I don't necessarily want to, like, show the information. We just need, like, a nice editor. Right? And this might be, like, something where we throw throw it back on, like, not markdown, but HTML. Right?\u003C/p>\u003Cp>View three markdown editor. View three markdown editor. Should probably just drop something like tip tap into this. Yeah. Let's just go for it.\u003C/p>\u003Cp>Okay. So PMPMI, MD editor MD editor v three. Here's what the usage looks like. Let's try to import all of this. This will be the content, And we're gonna what?\u003C/p>\u003Cp>Clone that content. Okay. Is editing. So this will be v m d editor. Is that what it is?\u003C/p>\u003Cp>V model content. And then we're gonna have a save function. Async function. Save. Await update card, which is a new one that we do not have.\u003C/p>\u003Cp>Update card. ID and content. Is editing value. Cool. I don't think there is a save function.\u003C/p>\u003Cp>Alright. So we're let's wrap this. Div. V f is editing, and we'll do a button to save. Now is this actually going to go off?\u003C/p>\u003Cp>I don't know. Vite server did not start. Something bad is oh, you gotta actually fire up the dev server after you install the thing, Brian. That would actually make sense. Fifteen minutes left.\u003C/p>\u003Cp>Can how far can we get on this internal knowledge base tool? Alright. So here we go. Search cards. We can we're not getting the card.\u003C/p>\u003Cp>We're throwing an issue. Use ID. Can I use list is outdated? Where do we get in here? A day card ID equals rent route top params.ID.\u003C/p>\u003Cp>Error when updating dependencies. What is the view version of this? Okay. So let's do PMPM. I think it's v view 3.5 view at latest.\u003C/p>\u003Cp>Hopefully, that gets us the use ID composable that we're talking about. PMPM dev. Okay. Syntax error does not provide an export. Alright.\u003C/p>\u003Cp>So that is obviously a problem. We will export update card. There's the data. Update the card, return the card. Bada bing badda boom.\u003C/p>\u003Cp>We are going to update the card with the value as well. Constant response, card dot value equals response. Or we can just console log this. And you could see Cursor is doing a lot of the heavy lifting for me here. What else are we getting?\u003C/p>\u003Cp>Service does not provide. It does provide an update card. Export async function. Services pages card. It should provide if we can get the card, why can't we update the card?\u003C/p>\u003Cp>Alright. So we hit edit. There we go. We can hit save. Card dot value equals response.\u003C/p>\u003Cp>V equals true. Shallow false. Update card. Why is the card Card. Well, that doesn't look right.\u003C/p>\u003Cp>On mounted card, await card response card dot value equals response. Why is this a why was this an array to begin with? Right? Extensions reload. Okay.\u003C/p>\u003Cp>Something is going wrong here. What are we doing wrong? The card dot contents, get card returns the card, read item cards dot ID. What is the actual response that we're getting? Inspects cards.\u003C/p>\u003Cp>There's the response. Card dot proxy. So why is it not showing the actual card content anymore? Car dot value. Oh, what is going on here, man?\u003C/p>\u003Cp>Do I have something else going on? But I define card way too many times. Response get card. So we get the card. I could see the response.\u003C/p>\u003Cp>The response looks good to me. Right? Go here. Go home. You see the the response.\u003C/p>\u003Cp>There's the response. Content. Tags. There's the card value. Maybe we set this to content dot value equals card dot value dot content.\u003C/p>\u003Cp>Card. Card dot title. What in the world is going on? Dep. Why are we loading?\u003C/p>\u003Cp>I'm not comprehending something here. VF card. VF card. We are seeing the car. Cannot read.\u003C/p>\u003Cp>VF card. How is the card dot title this is what I'm struggling to understand. Cards. There's the cards. There's the card.\u003C/p>\u003Cp>Card dot value. We can see the title right there, card dot value. Unref. Guard. We are running out of time.\u003C/p>\u003Cp>What is going on here, man? Where did we go wrong? Is it when we brought in the MD editor business? Alright. So this works.\u003C/p>\u003Cp>Yeah? Or it should. Some new, like, quirks with Vue. What is going on? I pulled in the latest version of Vue, and now we are, like, dying here.\u003C/p>\u003Cp>Dying on the vine. Car dot value equals response. Is there, like, a weird thing that I'm not picking up what I'm putting down here? Why does this work? And that doesn't.\u003C/p>\u003Cp>Cards ref. This is the exact same logic from the home. On mounted async, we get the card, Cards dot value dot response. Is this storing is like a lots of fun all the time. But cards.\u003C/p>\u003Cp>Click the card. See the card. There's the card. It is an object as it should be. Card dot value equals response.\u003C/p>\u003Cp>I don't understand. V f card has a value. Do we have to specifically say the value dot title? No. You shouldn't have to.\u003C/p>\u003Cp>Not in the template. Why do I keep getting this error? This would be a good question for AI, I assume. Why do I keep getting this as my content. You shouldn't have to do that inside the the template, though.\u003C/p>\u003Cp>Yeah. That's what I thought. You shouldn't have to unref in the template. Right? This is the case where AI, yeah, Check that response on your unmounted callback.\u003C/p>\u003Cp>Yeah. Huge help here. Some kinda, like, proxy issue with view, I'm assuming. I don't know. I don't know what the deal is.\u003C/p>\u003Cp>Read item cards. Card equals response. Blah blah blah blah blah blah blah blah. Okay. Some funky business going down.\u003C/p>\u003Cp>Ref car. Value. Yeah. There should be no need to actually I swear this is like a hot module reloading thing. B is ref true.\u003C/p>\u003Cp>What is the actual card, Joe? So if that's the card, why do we have? This is baffling me, man. So there's my card data. If that's the value of the card, should I not be able to access the card dot title?\u003C/p>\u003Cp>Card dot content. Oh. Oh my gosh. So that is the freaking problem when I updated and saved this card. This is like the king of deep oh, look.\u003C/p>\u003Cp>Sometimes. So I accidentally saved the value as that. We had everything going for us. I blew it all away just to goof this thing up big time. Hey.\u003C/p>\u003Cp>That's how we roll here on 100 apps. There was an issue with the save function that called caused the problem here. So let's say we got content equals ref. That'll be nothing for now. The content dot value should equal the response dot content.\u003C/p>\u003Cp>So that's the content we're gonna submit. Is editing. God. What an idiot. Boom.\u003C/p>\u003Cp>Explosion. There we go. This is one that I man, I really just have to, like, finish this one for my own sake. Failed this mission totally because I got hung up and not actually paying attention. That is the fun of this thing.\u003C/p>\u003Cp>Right? So here we go. We got an async function for save. Let's call it save. We're gonna update the card with the content.\u003C/p>\u003Cp>We'll set is editing to false. Const response response equals. We'll set the card dot value to the response. Cool. And then let's do this where we say v if is editing, v else model.\u003C/p>\u003Cp>We're gonna do wrap that in a div. V if is editing, and then we're gonna add a save button. Class p two blue, etcetera. Click save equals content dot value. Do we have a button?\u003C/p>\u003Cp>We forgot to add our edit button. And let's remove this at the top. Give this some padding. V is editing equals false. Vhtmlcard.content.\u003C/p>\u003Cp>Here's our content for this card. This is amazing content. Open this back up. There's our REST API. We hit edit.\u003C/p>\u003Cp>This shows this freaking thing. There's our editor. Test. Me. I failed the submission.\u003C/p>\u003Cp>Great. If we hit save, what does that do? Console update card is not defined. Obviously, we didn't import that. Update card.\u003C/p>\u003Cp>We'll try this again. Test, test, test. Save. Of course, there it is. There's the test, test, test content.\u003C/p>\u003Cp>Too little, too late, friends. And that's the way it rolls on 100 apps, one hundred hours. That's it for the internal knowledge base episode. Careful of how you say values, man. I never, for the life of me, thought, okay.\u003C/p>\u003Cp>Why am I getting this? Surely there would have been an error, boneheaded mistake. That's the way it goes. We'll see you on the next one.\u003C/p>","And we are back with another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. Welcome back. I'm here for Directus, by Directus something something. I I don't know. I'm trying to be fancy here. Not my forte, obviously. Dad of three little girls, gave up being cool a long time ago. But today, we have got a cool episode for you. We are going to be tackling an internal knowledge base. I'll explain what that is in a moment, but if you're new to 100 apps, one hundred hours, there are two basic rules. Number one, we have sixty minutes to plan and build your app, your clone, your idea, whatever this is. You get no more, no less. And rule number two, the anti rule. The no holds barred WrestleMania action, use whatever you have at your disposal. This is a no holds barred cage match, basically. So let's talk about internal knowledge base for a moment. When I used to run a customer success team, at a previous company, there was this product called Guru that was basically an internal knowledge base. You know, we used it for a lot of our chat support team. Also our CSMs, you know, if they had a question, we could easily document this. And the cool thing that they did have was this Chrome extension that would basically give you access to all the knowledge base. And, you know, the structure was very bite sized pieces of information, and that was the whole idea behind it. So we're gonna try and replicate something similar. You know, anytime you have something like an internal knowledge base, if it's not in everyone's face, the knowledge goes stale. If it's a pain in the butt to edit, it goes stale. So let's tackle that challenge. We're gonna put sixty minutes on the clock. Let's let's ride. Let's wrassle. Alright. So, functionality for this specific app. Right? And this is me probably biting off a lot more than I can actually chew in sixty minutes. But what we're gonna do, we're gonna build a back end for the internal knowledge base, And then we're going to build a Chrome extension to search search articles, edit articles, and that's probably the extent of maybe what we'll get done in an hour. I don't even know if we'll get this done. Right? But, we need to kinda flush this out further. Let's go in and map some data model. Map this out. Right? So at a high level, we're we're gonna have users of this application, so we'll have some authentication, etcetera. You know, they'll be able to log in. As far as the actual knowledge itself, I like the term cards for this. Instead of articles, that's what we're gonna do. And the reason why is these are bite sized pieces of internal knowledge. I think that's a helpful paradigm. As far as the fields that we want, we're gonna have a title for the card. Maybe I won't even get into, like, AI generated summary of this. We may use some AI stuff to, like, dive into this. Let's have an owner of this card. Like, who owns this knowledge internally? We've got the actual content of the card for sure. You know, maybe we got some tags for the cards as well. And then, of course, we have tags. Let's say there's a title for the tag and then a topic or an area. Right? I think that's a good structure for tags. Like, in in the case of Directus, that may be, like, features or frameworks or something like that. And then we'll have users. So and a user owns a card. Tags are connected to cards in some form or fashion shape, form, or fashion to quote the West Virginia governor here. This is a many to many relationship between cards and tags. Right? A card could have multiple tags. A tag could have multiple cards or a single tag could be applied to multiple cards. So that is the data model setup we are going to roll with. You know, if you've watched this before, I've got, like, a standard Nuxt starter that I use for this. But, on the front end, we're gonna try this Chrome extension Vite starter from, mister Fayezara. Fayez Fayez Ahmed. I think that's his name. Yep. Fayez. Hopefully, I'm pronouncing that right. If I'm not, I apologize. But, let's focus on that back end first. Right? We are going to zoom way in and let's create our first collection. Right? Let's call this cards, and I'm gonna give these a generated UUID. We'll say when the card was created, who created it, when it was updated. What's the status of the card? You know, this could be helpful if we have, like, cards that are draft or not approved or not verified, I would say. That should be like the one of the first states that we have for this. And then we'll add a a sort order, I guess. Why not? Let's see if we could find a card. So this is a looks like a memory card, a SIM card. It's a good icon for this. Great. I'm just gonna hit command s to save this and stay. And let's start with the title. So straight input field string. We do wanna require a value for this. Right? Great. You know, we could potentially index this field if we want to inside the database. That's what that's for. Do we need to trim? We don't need this to be URL safe, so we're gonna roll with that. And then what else do we have? We had an some content for this. Let's go with the WYSIWYG editor. We'll have some content, you know, and for, like, AI heavy stuff, we might even, like, set this to be markdown. Basically, behind the scenes, these use the same type for the column inside the database. And what Directus is doing behind the scenes whenever I add this field is basically just creating a new column inside the cards table inside our Postgres database. So for now, let's, let's go to the markdown interface just to appease our developer friends. And let's say we'll auto format the display for this. Cool. Great. Gravy. Amazing. What else do we need? Right? We wanna assign ownership of this card. And, you know, maybe we even want to have, like, a date verified. So let's take care of the ownership first. Right? I'm gonna add a many to one relationship here. So I'm gonna just call this the owner of this card, the related collection. Whenever you spin up a Directus instance, it gives us, some system collections that it prefixes with Directus so that all of your your SQL stays pure, which is nice. And then we're just going to add the Directus users collection here. So this is what the instance uses for authentication, you know, into the studio and via the API. So we'll just piggyback on that. Right? We'll assign a user to this. And for the display, cool, great, everything looks nice. We'll display should be an option to display the user. We could show the user in a circle. That will look great. And let's move that owner beside this. We want to show the status. Date updated. So there's the status title owner. And maybe we want to switch that around. Basically, what I'm doing here is just controlling the form for anybody using the back end of this. So let's make the title full width just to give that proper weight, proper gravitas. Here's the status. Maybe we wanna update that from draft to you know, it's not necessarily like we're concerned about whether this is published or not. We just want it to be verified. So, like, internally, you wanna make sure that that content is totally verified. So that's what we'll do. We'll call it verified. This is archived. Cool. We could copy these choices and just paste those into our labels. And I don't wanna show as a dot. I just wanna show each actual label. Alright. So next, maybe we add a time field, date time. Let's do this as time stamps. We'll just follow the same convention, date verified. Cool. Awesome. Alright. And we'll come back to that in a moment. This is our cards collection. Right? Let's call this direct us features. I'm gonna assign this to me, the admin user, and we will here's all the amazing features of Directus. Great. REST APIs, GraphQL APIs, APIS APIs. There we go. Insights, permissions, whatever. Right? Great. There's our preview for the markdown. You know, if we decide to do some AI, you should later this is gonna be a a friendlier format for that. Great. We've got a card. Cool. We can move this around. You know, let's add just a a hook to this using some flows to automatically populate the date verified. Right? So, anytime I verify a card, let's say a card is updated, updated or created, card is updated, created. You know, we could be add verification stamp. Alright. And basically, anytime a card is created or, you know, let's just say updated in this point because the default should be like draft, the state for all of the cards. Anytime a card is updated, let's go in and let's make sure check the status. Check status. Alright. We're just gonna create a condition here and this uses the standard, filter rules inside Directus. What I'm gonna do is just copy our template here. We're gonna adjust this a little bit. Payload dot status. We'll drill in one more layer And if the status is equal to verified, we're gonna run some action after this, right. So inside Flows is a great way to add simple automations to your back end And there is flow control already built into flows. Shocker. Right? But if this condition passes, we're going to take action on it. If it doesn't pass, meaning the status is not updated or it's not there, it will, you know, follow the rejection path. So in this case, you know, I I could log something to the console. In this case, I'm just going to, run a little script. Get current time. Right. We're gonna return, what's new date? Is that gonna be two ISO? Is it two ISO string like that? New date to ISO string. Is that what we're looking for? New date to string to ISO string. That should be what I think that's what we're looking for. So we're just gonna return the current time. Great. And then I'm going to update the data. Update card. Cool. So we'll do cards. We can use the permissions from the trigger. And I'm gonna say this should be dollar sign trigger dot keys, the first item in that array. Not a % sure, but, date verified. And then we'll do something like this. We'll just use this mustache syntax, git underscore current time. Great. Cool. Let's see if this actually works. Alright. So this is good. These are all amazing features of Directus. I'm gonna verify this. I hit save. And voila. Now we could see the exact date and time that I verified this thing, December 3 '10 '14 AM. Amazing. Sweet. Right? Alright. Let's go back and continue our March onto our data model here. So let's set up some tags. I'm not really concerned about when these were updated, etcetera, blah blah blah. You know, maybe we wanna sort the tags, but, let's find a tag. We could use the hashtag. That's what tags are for. This is a title. Cool. Title of the tag. That's required. And we'll keep keep updating. We don't wanna change just yet, my friend. And then the topic. Right? What are we gonna call this? Area topic? I think I like topic for this. I still wanna keep the string. I don't think there's a need for extra topics, but what we'll do, we'll just pick the options from a drop down. Right? So in the case of Directus, you know, this could be features features of Directus. Makes sense. Frameworks, like next Nuxt, etcetera. Frameworks. Can we get another f one? What else is this gonna be? API, API, pricing, you know, the pricing of direct you know, these could be infinite, and maybe that's, you know, not not a great one because that could be just like a a side effect. Cool. So now we have tags. We want to create this relationship between the two, and it doesn't really matter where I start here. All roads lead to the many to many relationship. If I'm on the cards collection, I'm gonna create a new field. We're gonna call it keys or I'm sorry. Call the key is gonna be tags. I need more coffee today. The related collection is gonna be tags. We don't wanna allow duplicate tags. We wanna show a link to this item. But what I wanna do here, I'm gonna open up the advanced field creation mode, and you could see what's going on behind the scenes. Directus is creating a junction collection for us. I'm gonna change the names of these just for my own anxiety, I guess. So we're gonna call this card tags. So these are tags on a card. The junction field or the the foreign key is card for the card, tag for the tag, and then we're gonna add this corresponding field to tags as well. We're gonna call those cards, and then the sort is gonna be sort. I'll just set these to delete the junction table item in case we deselect a tag from any of these. There's no reason to keep, the extra info in this junction collection. Great. We'll display some related values and away we go. Right? Now I'm gonna set like a default display template here just in case we're using that in some of the views. We'll do the same for the tags. We'll give that a title. Maybe we wanna show the actual topic. Cool. Topic. Great. Boom. Boom. Boom. Boom. Boom. We've got all the relationships. We've got everything configured. This is a feature tag. We don't have a tag for features. Well, we have a topic for features. What is this gonna be? This is, REST APIs. Cool. Directus REST API. It's awesome. Cool. Alright. So there we go. Now we have some tags. We have title. We have some content. We have the tags here. I can see all the cards within that. This is looking freaking sweet as far as the back end. Like, if I want to fetch all my cards, I could just open up the direct assistance. I'll swap the admin side of this. I'll go to items cards. Here I can see all of that. And one of my favorite features of the REST API is the ability to do stuff like this where I can say, hey, I want all the root level fields. That's what this wild card is for. And then I also want all the fields for the tags. So you could see here, this is I'm actually going through the junction collection. So I probably wanna go one step further and do this. So I can actually see, okay. Here's my tags. Here's the tag. What's the title of the tag? What's the topic? Etcetera. And I, you know, that's not really recommended for production use. Right? I would probably wanna be more specific so that you don't end up with, like, six levels of recursion because you could see the cards start to show here. Tags dot topic. Cool. There we go. There's the tag. There's the title. Topic. You know, I could go in and keep going with this. You get the idea. You get the picture. Alright. Let's bring that back. So I've still got access to my direct as admin. And then we are going to fire this thing up. Is this gonna go well? I do not know. So we're gonna pull this up, and I'm probably better off, like, starting a new directory for this. CD sites. Cool. We're gonna, like, what? Git clone? Can I page? Git clone. The extension. Okay. CD, the extension, and code dot. Is that the, that's not gonna open it in cursor. We're gonna cheat. Is it really cheating? I don't think so. We're gonna use cursor today. Alright. So looks like what do we have? We got PMPMI, kinda your standard. So we'll wait for this terminal to pop up. PMPMI. Okay. Come on. Come on. Core pack. Do I have to enable core pack? Core pack enable? Core pack enabler. That's what I am. User local core pack target in this. What is going on here? CD my extension, cloned to local. PMPMI. What is going on here? Unexpected identifier core pack. Is this just like a bad terminal instance? PPMI. Yeah. Wild. Okay. I guess the shell wasn't loaded yet before I fired this thing up. Alright. So now we'll do p m p m dev. Never used this thing before, so it should be fun watching me sweat. What are we gonna do here? Then load the extension in the browser within the extension folder. Okay. So, I am using Arc. I'm gonna switch over to Chrome for this. I'm gonna drag this out of the way. You know what? I I do like Arc. There are some frustrations with Arc, but, you know, it is what it is. Let's go to where is it? Slash extensions? Extensions? We'll just manage our extensions here. Let's load and unpack extension. You can see I've got, like, Vue already installed on this one. Extension select. Failed to load extension. What are my docs saying? Extension extension dev, I guess. Let's try the dev folder. Okay. We got v extension. Cool. What is this gonna do? Can we pin to the toolbar? Tim the toolbar, Taylor. Okay. Cool. So now we have this cool little extension. Alright. Dark mode, airplane mode, VPN. I don't know what any of these settings are doing, but, this looks cool. This is our little pop up widget. Right? I'm assuming somewhere in the source folder here we can find what we need for this. Alright. So this is kinda structured. I I've worked on Chrome extensions a little bit in the past. Let's see. We've got the pages. We got an app. So we're showing a router view, which is nice. So we can navigate between these specific things. We've got pages, index.HTML. We have the module. That's the specific pop up. You know, we've got, like, content scripts and things like that. I'm assuming if I open up this, this will say hello from content script. Let's actually for now, let's just focus on the pop up side of things. Right? I don't so much care about the settings here or the about. Where's the about page? That's the profile page, I'm assuming. Profile page. Put your own content here. Cool. Alright. How are we gonna get started with this thing? Let's see if we can actually just fetch a list of our articles, and we're gonna take over the home page here. The first thing I'm gonna do is move this script setup to the top. We'll do lang t s typescripts. Great. This has already got Tailwind installed, so I'm cruising there. Let's call this the we'll give this a little header actions. Is it articles, cards? Directus knowledge cards. That sounds great. Knowledge cards. Okay. Div. Great. Yeah. We'll do flex call. Okay. I'm I'm gonna roll with you there. Now as far as the components, let's not over engineer first. Okay. So now we get into, like, our first kinda inflection point here. We want to PMPM install the Directus SDK. Awesome. We'll put the SDK in here. We'll import where am I gonna stick the SDK clients? Util services, you know, we could call it that. Services. We will create a directus.ts file. We're gonna import the rest. We're gonna create Directus from the Directus SDK. Read items, read item, update item. I'm just gonna import the whole house here. Right? Create an item, delete an item. And one of the nice things about the Directus SDK is how modular it is. Right? So I can only I only have to import the items that I actually need. And I'm just gonna hard code the app URL here. That's our Directus URL, 8055. Create Directus. Okay. And then we can export Directus. Do we want that to be you know, let's call it export cons. So I could still, you know, access Directus. But also let's, let's carry this further and just get like a a function. Export async function. Get cards. Get cards. Cool. Yeah. Maybe we wanna potentially pass like a like params for that. We're gonna commit typescript no nos here. We'll just slap any on there. And what we're gonna do here, we'll say cards. We're going to what? Await. Directus. That's not that's not a good auto completion cursor. Alright. So we're gonna say read items, knowledge cards, params. We're gonna return the cards. Get card. Yeah. There we go. Okay. Now it's starting to get the picture. Right? This is one of the reasons why I have adopted cursor is because once it zeros in on, like, the logic for how you do some of this stuff, it is pretty good about actually taking care of it for you. Right? It's not gonna be knowledge cards though. And that could be solved by actually giving it, you know, like a TypeScript definition of what our types are, which you could potentially grab from the Directus open API spec. Alright. So let's go back to the pop up. Now we've got the service. We're going to import get cards from at services. I hope that is the actual alias for this. We'll we're about to find out, honestly. Do we wanna do cards? Why would we do cards as a reference? Cards equals await get cards. And are we gonna pass any params? Let's just get all the fields for the cards. It failed. Services from Directus. Yeah. I kinda figured that was gonna be the case. Okay. Now we've got that. Let's just go in. Oh, we've got this. We could format this a little bit. One of the first things I like to do just to make sure I'm getting the data that I want is just output this thing. So we'll save. Refresh. Can I actually see the network requests that are occurring through this thing? Inspect? What does that do? Does that give me okay. So now we're here inside the oh, as soon as you throw the pop up away, it disappears. Inspect. Is there a way to save this? The pop up is gonna disappear on me. So if I go to here so wait. Get cards. Are we actually finding that? Pages pop up. Dot.services.directus.js. Is that where how we should import this thing? I don't see nothing. What's happening? Maybe we should actually wrap this in, try catch so we could see some errors. Right? Try return cars. We'll just log the errors out. And now if we pop open the console, right, should we see some errors happening? Console failed to load. This is forbidden forbidden. We can't access this. Why is that happening? I kinda thought this would be the case. Right? So by default, whenever I create new collections, Directus does not provide any access to those collections. Right? So if I look at the Public Access Policy, I got nothing coming from that. We got thirty minutes on the clock, so for now let's, let's just grant full access to this. We are working locally. You know, if I was going to basically go to production with this, obviously, I would implement authentication. Maybe we can circle back to that. But, you know, the core functionality here is being able to update, edit, and rearrange these cards. Alright. So now with those permissions populated, we save this again. Can we actually see what's going on here? Console component, setup component. I don't see, get cards. Is there a way to actually sticky the inspection window? How do I do that? Focus page. I thought there was a way to to keep this up regardless, but maybe not for the pop up window. Settings, type error, node. Let's just try to restart. Let's see what we got. Do we have to reload this extension every single time? Refresh? I sure hope not, but we will if we have to. Now I don't see any. So we should be able to, like, console log the cards. Here's the cards. Right? Should be able to see some kind of output from that. Inspect. Console. Scheduler flush view internals. It's not liking that for whatever reason. Right? Maybe we want to do on on mounted. Cause cards. Response. Cards dot value equals response. Cards dot value. Hey. Okay. Yeah. It doesn't like fetching when the component is created for whatever reason. Not sure why that is, but there we go. We've got our cards. We could see the cards here. Cool. I can also do what we talked about where I can say, okay. I wanna get the tag. And where I'm using the SDK, I can also do something like this where we have tags, and then I can provide, like, an object like structure. So within that, we have, tag, and then we have the title. Let's fetch the ID of the tag and then the maybe even the topic. Tags. Tag. Okay. So now we could see all the info we want. That's working correctly. Let's, let's actually render out these cards. Right? So we'll do, like, a div. V four in cars key card dot ID. What are you gonna come up with cursor? Title.subscription. No. There's not gonna be a description for end. Maybe we show the tags within that. So span b four tag in card dot tags tags. Cool. Rest knowledge card, v API. Class. Let's give them some padding. We'll add a light border, round, large, rounded, l g. See what that looks like. Okay. And this is actually gonna be is this gonna be router link? Router link is gonna be missing two, a car dot ID. Okay. And then we're gonna need pages. So what does our actual router look like? Right? Okay. Looks like cursor is doing its thing for me here. So the path dot card dot ID pages card. So we're gonna create a new card page. Cool. Card dot view. I probably need to import that as well. Card view. Oh, it is dynamically importing those. Okay. That's fine. We can stick with that. Vbase TS setup. Move my script up to the top. And then we can, like, nab this card thing as well. But for now, this should solve the challenge. Okay. So now we can see the card. There's a direct us REST API. Card is looking nice. Let's go back to this. Maybe we give the title a little styling. Text large, font bold. Okay. We add a little margin to the top of this, give it a border, give a little padding to the top. Okay. So now we got our cards. We're probably gonna want a, like, a search input as well. Search input. If I do something like this, what is cursor gonna do for me? Input, placeholder, search. We could search our cards. Cool. And then, you know, we might even wanna do something like this where we have a query, and then we're gonna add, like, a watch to this. Watch search new val. What are we gonna do as far as the fetching? It's actually await get cards unmounted. And what we could do on the new value, we are going to if new value, cars have we don't wanna search cars that way. We could just use the directus API for this. So let's set up the watch, just to make sure that watcher is actually working here. And I could at least do this on the Directus documentation so we got something nice to kinda look at. Right? If I change the search here, it should be updating watch search. It's not console logging that new value for the search. Why is that? Because I don't have a v model on the search input. Let's try this again. Console. Why is the search are we outputting the sir okay. So now we can start seeing that. Great. Does this thing have something like view use already plugged in for, like, a debounce? I don't see that. But alright. We're we're not gonna worry too much about the debounce right now, and we're just gonna ignore this. Like, let's get to the actual card view for now. And, god, I really wanna change that router, that footer. We'll just drop the footer off of this. Cool. Alright. So now we can see the cards. Here's the settings. Cool. Great. How much time are we cruising out? We got twenty two minutes left. Let's see if we can get to actually editing these cards. So, if we take some of our logic here from the actual home page, we can probably just reuse a lot of this. We could get card, card. We don't need a search. Get card. And, again, I'm not totally sure why this thing is not wanting to card dot value equals response. Why it's not wanting to fetch properly? You know, if we're using script setup, it should do that for us, but, I digress. It's at a h one. Text two XL border b. P b four. Cool. And does it sound like tailwind typography in it? I don't know. We're just gonna do div class. Text large. Oh, we can add a prose class in there just to see. And one of the main reasons I love Tailwind is just the ability to, like, quickly do stuff like this. Alright. Git card. We're gonna git card. That is in the services.directus.js. Git card. Right? Search is not defined. Why do I need search? Oh, because we're watching that value. We don't need to watch that business. Alright. Go to home. So refresh this. We go to the direct API. Why is the ID is not defined. Oh, okay. Yep. We need to pass the ID. So that's gonna be what? Const route equals use route params dot ID. Are we passing that? Yep. We do have that. Cool. So now we should be getting that ID. What is the oh, we had to import that. Forgot I'm not working in Nuxt. Nuxt does all the fancy auto imports for us. So we refresh. There it is. Directus REST API. We can go home. We can go back to that card. Cool. Alright. Let's do const is editing. Ref equals false. Does this have auto imports? I'm assuming it does because it is. Let's see. Auto import, components auto importing, feed plugin components. We'll just import ref to be safe. Right? Obviously, that's already working, but let's give this some padding as well, make this look a little nicer, and we'll add a little margin to the top of this bad boy. Okay. Alright. So now we have our content. We could view this. Let's add a button to edit this thing. Right? Flex items center gap for I'll just say justify between, and then we're gonna add a button. Okay. We can edit this card. That's cool. The border b will move here. Cool. Alright. And if we edit this, what are we gonna use for the editor? View markdown editor. View markdown editor. What do we have here? What do we have? This is, again, risky business. Markdown editor in v three. Oh, no. Alright. This is it's not markdown. Base editor. Where are the demos? Import? Base editor? Oh, I guess we're this just doesn't have, like, some nice syntax. Right? Code mirror editor? Yeah. This looks way is this too cumbersome for what we're looking for? I probably should have snuffed this out earlier. Right? View editor, MD editor for view. This one's got 1,300 stars. I don't necessarily want to, like, show the information. We just need, like, a nice editor. Right? And this might be, like, something where we throw throw it back on, like, not markdown, but HTML. Right? View three markdown editor. View three markdown editor. Should probably just drop something like tip tap into this. Yeah. Let's just go for it. Okay. So PMPMI, MD editor MD editor v three. Here's what the usage looks like. Let's try to import all of this. This will be the content, And we're gonna what? Clone that content. Okay. Is editing. So this will be v m d editor. Is that what it is? V model content. And then we're gonna have a save function. Async function. Save. Await update card, which is a new one that we do not have. Update card. ID and content. Is editing value. Cool. I don't think there is a save function. Alright. So we're let's wrap this. Div. V f is editing, and we'll do a button to save. Now is this actually going to go off? I don't know. Vite server did not start. Something bad is oh, you gotta actually fire up the dev server after you install the thing, Brian. That would actually make sense. Fifteen minutes left. Can how far can we get on this internal knowledge base tool? Alright. So here we go. Search cards. We can we're not getting the card. We're throwing an issue. Use ID. Can I use list is outdated? Where do we get in here? A day card ID equals rent route top params.ID. Error when updating dependencies. What is the view version of this? Okay. So let's do PMPM. I think it's v view 3.5 view at latest. Hopefully, that gets us the use ID composable that we're talking about. PMPM dev. Okay. Syntax error does not provide an export. Alright. So that is obviously a problem. We will export update card. There's the data. Update the card, return the card. Bada bing badda boom. We are going to update the card with the value as well. Constant response, card dot value equals response. Or we can just console log this. And you could see Cursor is doing a lot of the heavy lifting for me here. What else are we getting? Service does not provide. It does provide an update card. Export async function. Services pages card. It should provide if we can get the card, why can't we update the card? Alright. So we hit edit. There we go. We can hit save. Card dot value equals response. V equals true. Shallow false. Update card. Why is the card Card. Well, that doesn't look right. On mounted card, await card response card dot value equals response. Why is this a why was this an array to begin with? Right? Extensions reload. Okay. Something is going wrong here. What are we doing wrong? The card dot contents, get card returns the card, read item cards dot ID. What is the actual response that we're getting? Inspects cards. There's the response. Card dot proxy. So why is it not showing the actual card content anymore? Car dot value. Oh, what is going on here, man? Do I have something else going on? But I define card way too many times. Response get card. So we get the card. I could see the response. The response looks good to me. Right? Go here. Go home. You see the the response. There's the response. Content. Tags. There's the card value. Maybe we set this to content dot value equals card dot value dot content. Card. Card dot title. What in the world is going on? Dep. Why are we loading? I'm not comprehending something here. VF card. VF card. We are seeing the car. Cannot read. VF card. How is the card dot title this is what I'm struggling to understand. Cards. There's the cards. There's the card. Card dot value. We can see the title right there, card dot value. Unref. Guard. We are running out of time. What is going on here, man? Where did we go wrong? Is it when we brought in the MD editor business? Alright. So this works. Yeah? Or it should. Some new, like, quirks with Vue. What is going on? I pulled in the latest version of Vue, and now we are, like, dying here. Dying on the vine. Car dot value equals response. Is there, like, a weird thing that I'm not picking up what I'm putting down here? Why does this work? And that doesn't. Cards ref. This is the exact same logic from the home. On mounted async, we get the card, Cards dot value dot response. Is this storing is like a lots of fun all the time. But cards. Click the card. See the card. There's the card. It is an object as it should be. Card dot value equals response. I don't understand. V f card has a value. Do we have to specifically say the value dot title? No. You shouldn't have to. Not in the template. Why do I keep getting this error? This would be a good question for AI, I assume. Why do I keep getting this as my content. You shouldn't have to do that inside the the template, though. Yeah. That's what I thought. You shouldn't have to unref in the template. Right? This is the case where AI, yeah, Check that response on your unmounted callback. Yeah. Huge help here. Some kinda, like, proxy issue with view, I'm assuming. I don't know. I don't know what the deal is. Read item cards. Card equals response. Blah blah blah blah blah blah blah blah. Okay. Some funky business going down. Ref car. Value. Yeah. There should be no need to actually I swear this is like a hot module reloading thing. B is ref true. What is the actual card, Joe? So if that's the card, why do we have? This is baffling me, man. So there's my card data. If that's the value of the card, should I not be able to access the card dot title? Card dot content. Oh. Oh my gosh. So that is the freaking problem when I updated and saved this card. This is like the king of deep oh, look. Sometimes. So I accidentally saved the value as that. We had everything going for us. I blew it all away just to goof this thing up big time. Hey. That's how we roll here on 100 apps. There was an issue with the save function that called caused the problem here. So let's say we got content equals ref. That'll be nothing for now. The content dot value should equal the response dot content. So that's the content we're gonna submit. Is editing. God. What an idiot. Boom. Explosion. There we go. This is one that I man, I really just have to, like, finish this one for my own sake. Failed this mission totally because I got hung up and not actually paying attention. That is the fun of this thing. Right? So here we go. We got an async function for save. Let's call it save. We're gonna update the card with the content. We'll set is editing to false. Const response response equals. We'll set the card dot value to the response. Cool. And then let's do this where we say v if is editing, v else model. We're gonna do wrap that in a div. V if is editing, and then we're gonna add a save button. Class p two blue, etcetera. Click save equals content dot value. Do we have a button? We forgot to add our edit button. And let's remove this at the top. Give this some padding. V is editing equals false. Vhtmlcard.content. Here's our content for this card. This is amazing content. Open this back up. There's our REST API. We hit edit. This shows this freaking thing. There's our editor. Test. Me. I failed the submission. Great. If we hit save, what does that do? Console update card is not defined. Obviously, we didn't import that. Update card. We'll try this again. Test, test, test. Save. Of course, there it is. There's the test, test, test content. Too little, too late, friends. And that's the way it rolls on 100 apps, one hundred hours. That's it for the internal knowledge base episode. Careful of how you say values, man. I never, for the life of me, thought, okay. Why am I getting this? Surely there would have been an error, boneheaded mistake. That's the way it goes. We'll see you on the next one.","236f7d6a-3c0e-4965-9534-a8ca7de26efd",[634],"81e81b72-9749-40e1-a9ea-6698718110af",[],{"id":142,"number":143,"show":122,"year":144,"episodes":637},[146,147,148,149,150,151,152,153,154,155],{"id":152,"slug":639,"vimeo_id":640,"description":641,"tile":642,"length":643,"resources":8,"people":8,"episode_number":305,"published":556,"title":644,"video_transcript_html":645,"video_transcript_text":646,"content":8,"seo":647,"status":130,"episode_people":648,"recommendations":650,"season":651},"website-personalization-engine","1059436796","Bryant builds a content personalization system that shows different variations of web content based on visitor segments. Watch as he uses Directus to create a flexible backend for managing segment-specific content variations, then implements client-side detection to serve the right content to the right visitors.","40c50d05-f056-49ff-a4e8-cb7db67cbdc1",61,"Mission: Website Personalization Engine","\u003Cp>Speaker 0: Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie, here from Directus. And today should be well, I think it's gonna be an exciting show. My confidence level on this one, just to put that out there, is fairly lower than normal. But today, we are going to be building a website personalization engine.\u003C/p>\u003Cp>I went through several different titles for this one. I don't know if that's actually gonna stick when this goes on Directus TV or not. But, what are we trying to do? Well, first, let's explain the rules. If you're new to 100 apps, one hundred hours, the rules are simple.\u003C/p>\u003Cp>Basically, we have sixty minutes to plan and build a clone, an application, a website. So that is plan and build, nothing more, nothing less. And then the only other rule, which is the I call it the anti rule, is use whatever you have at your disposal, which I will probably be leveraging, some AI today because I am wading into uncharted territory for me. So website personalization engine, sixty minutes. Let's do this together.\u003C/p>\u003Cp>Or it'll, like, publicly laugh at Bryant. Try. Alright. So we're gonna put sixty minutes on the clock, and let's cover this. Alright.\u003C/p>\u003Cp>So website personalization, you know, originally, this idea was for, like, personalized landing pages. Like, I know a specific company or target account that I want. Let's create a specific landing page for them. But in the light research that I did before this, which is basically, like, ten minutes of Google, basically, when it comes to personalization, it's more of a segment based approach. So that's what we're gonna sketch out here.\u003C/p>\u003Cp>You know, we wanna bucket visitors into a segment and then display certain content for that spec segment to personalize it. So, let's plan out our kinda data model. And, you know, I could leverage, like, some of our direct to CMS starters here. We're just gonna keep this stupid simple super simple. I tell my kids not to say stupid, so I probably shouldn't say that either.\u003C/p>\u003Cp>But alright. So as far as our data model, let's flesh out the application. Maybe we do just wanna, like, flesh out what are the actual features that we want out of this specific application and make that less huge. Alright. So we want to have a piece of content.\u003C/p>\u003Cp>Great. Directus will manage that for us pretty easily. And then we want to show variations of that content based on traits of a visitor or segments segments that a visitor belongs to. Now, something like this or or even when you get into, like, AB testing, you know, it's fairly easy. I won't well, I won't say fairly easy, but it's easy enough to set up, like, an AB testing code.\u003C/p>\u003Cp>If this, do that. If this if else, do that. But, like, the intersection of, like, handing this off to a content or a marketing team and setting it up in code and, like, managing all of that to work together for front end, back end, and making it cohesive, really difficult task. Right? So this is really all I'm trying to achieve today.\u003C/p>\u003Cp>We could make this fancier maybe down the the line, but let's let's discuss the data model for this. What are we gonna set up? So I could just call this content. Maybe it's, let's just have a pages collection. You know, this could be like a post or something like that.\u003C/p>\u003Cp>And then we're going to have what else? We're probably gonna have some segments. Like, here's the different segments that we want. And, you know, if you were doing, like, a proper system, you'd probably want something like events. Not super concerned with that.\u003C/p>\u003Cp>You know, the segments probably have, like, some rules for each segment or yeah. What are the rules? This sort of thing. I don't even know if we'll get this far. Let's just try to wade through this and and show something on the page.\u003C/p>\u003Cp>Alright. So let's get into what I've got set up. This is, just a Nuxt application. This is my standard starter for, you know, like, if I was just YOLO ing into a hackathon, this would be my starter. You could see a bunch of commented out code.\u003C/p>\u003Cp>But basically, I've got a blank Directus instance, if I can actually get logged into this bad boy. Directus is gonna be our CMS. It's gonna handle all of the back end for us. Dear lord. Why can I not get logged into this thing?\u003C/p>\u003Cp>Boy. Boy, oh, boy. Admin example. Password is the password. It's not even up, but that would be why that would be why I couldn't get into the application.\u003C/p>\u003Cp>Alright. So I've got Directus set up, in a Docker Compose, just a standard local setup kind of format. And like I said, Directus is gonna be our back end, our CMS. Okay. Great.\u003C/p>\u003Cp>Blank slate. Nothing fancy here. We do have templates available for stuff like CMS if if that's something that you're into. And then we have a Nuxt website. Alright.\u003C/p>\u003Cp>So let's start building some functionality. And inside the Nuxt website, we've got, like, an index page. So that's what we're staring at here. Alright, so let's add some pages content. Great.\u003C/p>\u003Cp>Go in, we'll create a new collection inside pages. For those of you who are new to Directus, what's happening behind the scenes here is Directus is connected to my Postgres database and it is basically gonna mirror whatever changes I make here to that SQL database. And I could connect like a an existing SQL database to this as well. So we have some pages. Great.\u003C/p>\u003Cp>Got a collection that's gonna show up here. When I hit new, there's nothing shows up. So we need to add some fields to our pages. And again, like, we're modeling our data here. We're generating an API and we're building a form for our content editors all in one place.\u003C/p>\u003Cp>So page needs a title. Great. A page needs a slug. So we're just gonna create a new field called slug. I'm gonna go into the interface settings and make sure this is URL safe.\u003C/p>\u003Cp>Do I have my little pointer? Yeah. I do. That's a cool little tool. Occasionally, I get questions on it.\u003C/p>\u003Cp>It is called mouse pose, Mac only, I think. Alright. So what else are we gonna have on our pages? We got a title. We got a slug.\u003C/p>\u003Cp>Maybe we'll keep this simple. Right? We've got a headline. That could be some text. Cool.\u003C/p>\u003Cp>And because I am totally gonna make it easy on myself, we're just gonna call this content. One big rich HTML rich text content block. This probably looks more akin to, like, a blog post than a page. Directus does have that amazing many to any like page builder scenario that's covered in some of our templates but we gotta crawl before we can walk. Alright so we got our pages, lets go ahead and let's just create a page.\u003C/p>\u003Cp>Right? This is Brian's page. Brian's page. Cool. Hey, yo.\u003C/p>\u003Cp>Why am I talking to myself? Great. Alright. So we got some content. Now if I want to query that via the API, I can go to the directus URL, go to items, pages, and boom.\u003C/p>\u003Cp>I could see I have a page here. There's my content. API is ready to go. This is already I've I've got the Nuxt application wired up with, like, a simple Directus SDK client. So we're creating Directus, we're using REST, and then we're providing that to the Nuxt application.\u003C/p>\u003Cp>So So if I go in and I do something like this, where we're just gonna create a new dynamic route inside Nuxt and do I have, like, a v t s? Yep. Script set up. Really need to work on my snippets to save time here. And my script at the top guy, when I'm working with Nuxt review.\u003C/p>\u003Cp>Alright. So now we want to fetch some of this page content. Right? Because what happens now, I've got this. If I go to Bryant's page, nothing shows.\u003C/p>\u003Cp>Yeah. That sucks. We need to resolve that. Alright. So what we're gonna do, we will do const can't type today.\u003C/p>\u003Cp>You'll notice I am using cursor here. Directus equals use Nuxt app. And, okay, we've got some auto completions that are not necessarily gonna give us what we want. Alright, so we're gonna get the data. Nuxt has the use async data function or the composable that we're going to use.\u003C/p>\u003Cp>If I can actually type that, great. We're gonna give this a key. So, the key, if we grab the routes, is going to be the slug, route. Params. Slug page.\u003C/p>\u003Cp>Let's just tag this as a literal. There we go. Thank you. Alright. And then we are going to return directus dot request, we're gonna do read items pages.\u003C/p>\u003Cp>Pages. K. And we're gonna filter where the slug, that's right, is equal to route dot params dot slug. K. So we have direct us there.\u003C/p>\u003Cp>We're gonna import read items from our SDK. You know, I could potentially, like, import that here and provide that as well if I wanted to, but we'll just do it this way, keep it nice and easy. And then let's see if we can just actually get the data. It'd been a minute since I messed with this. Is this actually going to work?\u003C/p>\u003Cp>Are we making a network call? Oh, wait. Use async data. There's that. There's the call that we're returning.\u003C/p>\u003Cp>Read items pages. Let's check our network requests. Are we seeing any actual network requests? It is server rendering these. So we are not seeing that.\u003C/p>\u003Cp>Probably want to add some error handling. So we'll just add that. If there's an error dot value, we are going to console dot error, error dot value. Great. Do I have any formatting set on this?\u003C/p>\u003Cp>Got a little bit of formatting. Alright. Can we see the error? H three error create error. What is going on there?\u003C/p>\u003Cp>There's an error. Error dot value dot message. What's the format for this? Create error. We're throwing an error.\u003C/p>\u003Cp>Hey. I'm guessing though, just from past experience, this is because we have not enabled any access to that page's collection. Right? So when I do this, I am still using the session token from Directus. But if I open this up in a new browser, we could see, hey.\u003C/p>\u003Cp>I don't have any permissions. So I'm just gonna enable permissions for pages. Let's do read permissions, see what we got. Boom. We can actually get some data and wrap this in a nice pre tag to see what we've got.\u003C/p>\u003Cp>Okay. Cool. Now let's take this data that we have and we'll add an h one. This will be the page dot headline. And then we are going to check and see what all I do have in this application.\u003C/p>\u003Cp>I don't have, like, a pros component. So we'll just do something like this where we have VHtml content. Now if I refresh if I refresh if I refresh, why is it that actually showing? Where's my content? Page dot headline.\u003C/p>\u003Cp>How is it that I could see the page content? Oh, I do have it here. Page dot headline. Why isn't that actually showing on the page, though? Interesting question.\u003C/p>\u003Cp>Interesting. Definitely. Let's just center this up. Maybe let's max auto, max width, four x up. Add some padding.\u003C/p>\u003Cp>Why isn't my content displaying via page? Do we need to wait on the page? Am I really not smart enough to figure this part of it out? Is there some type of error that we're getting paged out headline? I could clearly see oh, yes.\u003C/p>\u003Cp>That's why, Brian. You are getting an array here, so we need to transform that. So async data. I think it's here that we can do a transform. Data.\u003C/p>\u003Cp>Data. Just the first item. There we go. Alright. There we go.\u003C/p>\u003Cp>Hey, yo. Why am I talking to myself? Make sure that you understand what your data looks like before you start trying to mess around with it. We're gonna make that giant. And if we slap a pros tag onto this, pros l g.\u003C/p>\u003Cp>I think I've got Telen typography included here so we could see this data. Okay. So we're rendering something out to the page. Great. Now, this is super fancy.\u003C/p>\u003Cp>Right? Let's just grab something from the Directus blog to use as content. Here's a post that I wrote. I'm just gonna copy this entire thing. We're gonna go in and throw that here.\u003C/p>\u003Cp>Great. Just to get something on the page. Okay. We'll call this context switching sucks for devs. Cool.\u003C/p>\u003Cp>Alright. So now we are showing a basic thing on the page. Hallelujah this is amazing this is the coolest thing we've ever built. How are we doing on time? We got about forty five minutes left.\u003C/p>\u003Cp>All right so we have a piece of content now we want to show variations of that content for an actual visitor. So how are we going to achieve this sort of thing? And, you know, if we boil this down to the basics, we've got a page. We're going to have variations of page content. Yeah.\u003C/p>\u003Cp>We could call this personalizations or page variations. That's gonna be linked to our segments, and this is not right. I do have a PhD in drawing arrows, just not in Figma. This is still a challenge for me. Alright.\u003C/p>\u003Cp>So let's create this and I think what we can do is hijack some of the functionality that already exists in Directus for this sort of thing. Going off label here, off brand. So, we will go into our data model. What I'm gonna do, I'm just gonna create a page variations. Yeah.\u003C/p>\u003Cp>Naming stuff is probably the hardest challenge in development. You know, we could call this personalizations, but that might make somebody mad. Yeah. I'll just do created at, created by. I don't necessarily need this information.\u003C/p>\u003Cp>These are just helpers that you can add. Directus, like, prefix or, like, sets these up for you. So whenever, you save this item or create a new item, it obviously records the user, etcetera. Just some shortcuts. Alright.\u003C/p>\u003Cp>So we have page variations. We've got two different collections here. Great. How are we gonna tie these things together? Now, what I can do and what I'm gonna steal is our translations feature.\u003C/p>\u003Cp>So if we go to the docs actually, let's go to the new docs. These things are in beta as of now. We're gonna look for translations. Great. We have the ability to translate content and manage all those translations for you.\u003C/p>\u003Cp>And if I open this in a new tab, we get, like, this beautiful side by side interface where I could see, okay, here's English versus Spanish or French. And this is like, when we boil it down, like, when I think about it, I'm just now coming to me through my head here, is it basically, translation is a form of personalization anyway. So why not leverage this existing structure? You know, short of, like, the icons and maybe some of the other stuff that is baked in there, I I think it should work. So I'm also going to like, normally, if we were doing translations, you would create, like, a languages collection where you would store all the languages that you wanna translate your content into.\u003C/p>\u003Cp>In this case, what we're gonna do is just use, I'm gonna create a new collection. I'm gonna call it segments. You know, we could manually generate the IDs for these or we could use, like, a generated UUID. You know, if I was trying to scale this out, I would definitely be using, like, generated UUIDs, but let's just call this key we're gonna manually enter a string for this. So whenever we create a new segment, we're gonna manually enter in the key.\u003C/p>\u003Cp>That'll help us keep things a little bit clearer. Updated at updated by just to keep track. And do we want this segment to be active or not? You know, maybe we set that up on a a Boolean toggle. Alright.\u003C/p>\u003Cp>So we got a key for it. Maybe we want, like, a proper name. So we add a name field and drag this down. And what I'm doing here is basically just configuring the actual form that we're gonna use to set this stuff up. Do we have a segment?\u003C/p>\u003Cp>Do we have a category? Yep. There we go. Yeah. Again, naming things is hard.\u003C/p>\u003Cp>Let's add some icons. Just the designer OCD for me kicking in. Doc. Yep. There we go.\u003C/p>\u003Cp>Edit document. And I'm just gonna like actually hide these page variations. So now we have pages, we have segments. Let's create a new segment. Right, I want to speak to developers.\u003C/p>\u003Cp>We could say these are for developers, that's our nice name. Directus is also helpful for content editors, so I'll do content editors. Yeah. I wish I was doing this live so I could say, hey, hyphenate or underscore, but let's just go with a hyphen. Fine.\u003C/p>\u003Cp>Fine. Fine. You know, I could keep adding these as much as I want. Right? So now we've got some segments, we've got some pages.\u003C/p>\u003Cp>We wanna generate these variations. So again, we're gonna reach for the Directus translations interface. And I hope this works out like I think it will or like I hope it will. You can see that we're pre setting the languages collection. Directus is smart enough to know, like, when you're doing translations, you need languages, we can create that for you.\u003C/p>\u003Cp>But what I'm gonna do here, I'm gonna open up advanced field mode instead, and I'm gonna go in and I'm just gonna do all of this mapping myself. So we try to be smart and create junction collections and everything for you, but you can take total control of this as well. So here, this is the collection. So we're gonna call this page variations. I'm just gonna use page and we're gonna call this the segment and the collection that we're targeting is segments.\u003C/p>\u003Cp>So, I'm gonna set these all up to cascade, so if we delete a page variation or deselect one, we delete that content because we're not going to use these variations across different collections or pages. Alright, so the language indicator field. Now normally this is what shows here right up here. Right? Just the indicator of what this is.\u003C/p>\u003Cp>We could use the key. We could use the name. Let's use the name. Language direction. We're gonna ignore that.\u003C/p>\u003Cp>Default language is the primary key. Let's let's say I wanna default to developers. And we're gonna start this split. So we'll start open like this. Great.\u003C/p>\u003Cp>Okay. So now we create this, and we could show related values. You know, you can even preview the translation content. And I already see one thing that's going to bug the crap out of me as I called it translations. I forgot to rename this.\u003C/p>\u003Cp>Alright. So we're gonna call this variations. So it's not translations, it's variations. We're just piggybacking off of functionality. Alright.\u003C/p>\u003Cp>Pages. Cool. This is gonna be the what? Key? No.\u003C/p>\u003Cp>Segment. We're gonna call that segment. That's the segment. The collection is segment. And I gotta set up this nice cascade again.\u003C/p>\u003Cp>Okay. Go through the exact same process all over again. Love shooting myself in the foot Just to make sure we have variations. Is variations spelled correctly? At this point, we don't know.\u003C/p>\u003Cp>Alright. So I probably goofed something up already. Did I? Did I not? Page variations.\u003C/p>\u003Cp>Page variations. Page page is not found. Oh. Yep. Alright.\u003C/p>\u003Cp>So love it when I goof up. Cause more work for myself. We're just gonna start this process all over. Alright. Try it one more time.\u003C/p>\u003Cp>Page, variations, Generate the UID. I'm not even gonna go for that. At this point, we're gonna say translations interface. Great. This is gonna be segments.\u003C/p>\u003Cp>We're going to use the segment name. We're gonna start with the split open view. And let's control this. Right? Page variations.\u003C/p>\u003Cp>This is gonna be page. Segment. Delete. Alright. So definitely don't do what I have done.\u003C/p>\u003Cp>Let's try this. See if that works. Doesn't have field page and doesn't have a relationship. And what in the world field page in collections doesn't have a relationship? Why doesn't it it should have a relationship.\u003C/p>\u003Cp>Pages, segments, translations, start open. Why doesn't this have a relationship? Well, let's just see what happens when we do this in orally. Translations already has a relationship. Alright.\u003C/p>\u003Cp>So we can see here by adding this we get a languages and then we get a pages translations, there's a pages ID, there's an ID. This should work out okay. I don't know what is going on. Maybe I didn't refresh the data model. Key name, segments, page variations.\u003C/p>\u003Cp>Why am I not getting this to work correctly? Let's just take a look at our database and see what's happening. 53Edit. 5 3 2, test, Connect. Alright.\u003C/p>\u003Cp>So now we're behind the scenes into the database trying to figure out what I goofed up to begin with and it looks like there's some relationships here that got left over that didn't get fixed that are causing problems. Alright. So now with that out of the way, let's try this again. Page variations, we're gonna generate an ID for those. I'm just gonna skip all the fanciness.\u003C/p>\u003Cp>And now I'm gonna go in and do translations. We're gonna call this variations. Instead of languages we're going to use segments, we're going to use the name of that. We are then going to set up to use page variations, page segments, segment. Okay.\u003C/p>\u003Cp>Now is that going to work out how we want fingers crossed. Let's log in and see what have we got. Okay. So now I get this side by side interface of content editors and developers. Right?\u003C/p>\u003Cp>But I don't see anything inside the actual form. So inside this junction table is where we're gonna go in and create our content that is actually going to vary. So in this case, it's just basically going to be our headline and our content. And I can quickly duplicate these. So we'll go to page variations.\u003C/p>\u003Cp>I'm gonna say copy headline. We're gonna copy the content. Just make sure you change that copy that we prepend at the end so that you don't, you know, get confused. But with this information set up, now I can get an interface that looks like this where we can say, hey. Great.\u003C/p>\u003Cp>Now we've got developers. We've got content editors. Cool. And I can be able to store different contexts here. Context switching sucks.\u003C/p>\u003Cp>Great. That's what we'll change the default headline to. We're gonna change this for devs, Context switching sucks for content editors. This content is for editors. This content is for devs.\u003C/p>\u003Cp>Alright. So now thinking through our mental model, we've got our default content. And then if we identify a segment, like if the visitor falls into the content editor segment or they fall into the developer segment, we're going to show them different content. Now, what is this actually going to look like? So if I go and like we look at the API now, got my API request, you could see those variations.\u003C/p>\u003Cp>One of the beauty, beautiful things of Directus is the ability to use the rest API in a GraphQL like manner. So I could do something like this where I just say, hey. Give me all of the root level fields and also give me all the fields within our variations. So now I can see here's my variations. The segment here is developers.\u003C/p>\u003Cp>The, segment here is content editors and presumably on the Nuxt side of it. And, you know, I could rig something up via the API to do this swap for me, but depending on how I'm rendering my site, you know, let's say I'm statically generating the site, if I want to still offer this, I've got to pre generate the payload, you know, pre generate our JSON and store that so I can use it when rendering it. So we're gonna try to do this on the client side. Inside our page level data here, right, we're still getting the same result. And if I add the data back, you know, now we should be seeing our variations, but hey, lo and behold we're not because again we need to go back into our access policies and set that up.\u003C/p>\u003Cp>So we're going to allow read access for our page variations and for our segments just so we could see those things. You know, when we're going to production, we'd probably make sure there's some kind of status on these so that, you know we're only showing published pages etcetera, but outside the scope. So now I can see my variations. We're gonna go here and let's just specify our fields, right. It's not content, it is variations of this content.\u003C/p>\u003Cp>And now we could see that. Great. Alright. So now we need to translate this content or personalize it in this case. Right?\u003C/p>\u003Cp>How do we assign a segment? You know, like, writing a rules engine here would be, kinda challenging with the twenty seven minutes we have left, I would say. But, you know, at the core, we we've gotta have some way to assign the segment. What I'm going to do I I think we're just gonna use a cookie, for this. So Nuxt has a use cookie composable.\u003C/p>\u003Cp>Let's just call this the segment key. We're gonna use cookie or you call that segment. Great. And I'm just gonna make this stupid simple. We'll add a div.\u003C/p>\u003Cp>We'll flex these, add some gap, make sure that's in the middle, etcetera. And then we're gonna add some buttons. I've got the Nuxt UI library in here. Segment key equals developers. No.\u003C/p>\u003Cp>We're not gonna do designers, but, we're gonna do content editors. Content editor. And then maybe we just add I am a great. Alright. Maybe we wrap this whole thing in.\u003C/p>\u003Cp>I think it's a u container from the Nuxt UI library. Okay. Yep. So we get a little bit of spacing there, And we can add, just some space between these space y eight. Okay.\u003C/p>\u003Cp>Great. And this is probably all part of the same section. Okay. So I am a developer. I am a content editor.\u003C/p>\u003Cp>I am a developer's. Great. Again, naming is crucial here. But basically, like, what I should be able to see now if I go into, like, my cookie storage, I can see that segment as well. Right?\u003C/p>\u003Cp>So based on this, I'm tagging that person. You know, and we're just explicitly asking. But, you know, we could set up, like, some kind of client side rule engine or, you know, you could potentially do this on the server side as well. Whatever pages you visit, weight those against each other and assign a, you know, if I've got five articles for developers and you look at three of those, then I'm probably safe to say that you're a developer, versus, you know, reading the content editor pages. Alright.\u003C/p>\u003Cp>So how do we actually translate this? Right? We probably want, like, a helper function that takes our page data and then evaluates the segment and then, you know, spits out the proper content. So let's just see how awesome AI has become here and see if this is actually worthwhile or I should just probably write it out. But, this wouldn't be fun if we weren't using something like cursor and, you know, something super opinionated or where everybody has an opinion about AI at this moment.\u003C/p>\u003Cp>Here we go. Write a let's just describe this. Right? Personalize the page content based on the segment key. Where is this gonna come up with?\u003C/p>\u003Cp>Personalize content, page value dot variations. I okay. You know, maybe this would come back. Let's see. Helper function to get the personalized content.\u003C/p>\u003Cp>Page variations return page dot that's not really content. Right? We just wanna return yeah. Okay. So I'm probably not prompting right.\u003C/p>\u003Cp>Let's see what we can do. Take in the page and the segment key and return a merged page with the proper variation based on the segment key. That's better, still not great. Right? You would probably want, like, a personalized function where it, just basically takes in, the page content and the different segments and the current segment and returns that.\u003C/p>\u003Cp>But nevertheless we got our personalized page or we should, right. Great. We can see our variations, we can see the content. If I check-in view dev tools, we go to our slug page. Do we have the personalized page?\u003C/p>\u003Cp>Great. That doesn't really Context switching sucks. No. But that doesn't look right to me. Right?\u003C/p>\u003Cp>So we do personalized page dot headline, personalized page dot content. Context switching sucks. I am currently in the developer side of things, so it should be showing should be toggling this. Right? So there's something wrong here.\u003C/p>\u003Cp>Page dot value dot variation. Yeah. So, basically, what we wanna do is loop through the trying to to do this the AI way and not get frustrated. Loop through the page variations and merge them with the page based on the segment key. Still showing the same thing.\u003C/p>\u003Cp>Personalized page. Yeah. Okay. Yeah. Alright.\u003C/p>\u003Cp>So this is the final page. Let's define that. The final page. Okay. Then we are going to is this getting smarter now?\u003C/p>\u003Cp>For constant of variations, if segment dot key, final page, return final page. What does that do? Why is it why is it still not doing what we want? Page.valuepage.value.variations.developers. Oh, yeah.\u003C/p>\u003Cp>That's why. Basically, we need to use, like, a file. Constant variations equals variations. Selected Constant variation is going to be we basically need to look through this array. Variations dot find.\u003C/p>\u003Cp>We'll look for the variation. And there we go. Okay. Yeah. So we're getting that array.\u003C/p>\u003Cp>That's the problem. So now if I remove this blah blah blah page, Now I have personalized content for folks. Amazing. This is the personalized content engine, website personalization engine. Badabing badaboom.\u003C/p>\u003Cp>This is beautiful. Right? So now I can define all of these variations within our actual CMS. Like here's the segments that we want to create content for. You know, I could potentially set up some type of rules engine for this.\u003C/p>\u003Cp>And then, you know, on our individual pages, then I can go through and define this content. Now is this a real world scenario? Probably not. You would take this and use something like our mini to any builder where you're building these dynamic pages and within each section you have personalized content for that. But at the very least, hey, this is now we've got, a pretty robust I won't say robust.\u003C/p>\u003Cp>This is not robust at all. But it is a a pretty it's a start to a personalization engine basically. That's it. That's all. Now where do we go from here?\u003C/p>\u003Cp>Right? I this would be extracted out into a, like, a helper function, so that, like, each page or each block, we would we would call this function, return the content if there is a variation, like, if we have a segment key. Yeah. And we could even go as far as, like, creating a rule engine for this. So let's just continue to beat on AI, which is, honestly not been great so far for this.\u003C/p>\u003Cp>Let's say extract this out to out to a helper function that is more robust, maybe. I don't know. Let's see. I'm gonna take a sip of coffee while it's trying to choke that down. The auto completions are not great, but here we go.\u003C/p>\u003Cp>Let's see what it's come up with. Personalization. This looks to be a composable. Use personalized page. Here's the page.\u003C/p>\u003Cp>Segment key. Update the page component. Here's how we're gonna use that. Personalize page. Use a page.\u003C/p>\u003Cp>There is a segment key. Okay. AI. I will bite. Alright.\u003C/p>\u003Cp>App utils. Personalization. And I don't know why it's using the is there let's see, like, an actual it's using the composable format for view, but import type maybe rest. Return computed. I mean, to me, this is a I guess this is a this is a composable.\u003C/p>\u003Cp>I'm not sure why it's doing that. So we'll stick that there. This is use personalization. This is the name of it. Use personalization.\u003C/p>\u003Cp>Again, probably not a strong choice for, like a composable here because this is probably something that you might want to extract out and use, like, across different projects. Use personalization. Use personalization. Okay. Cursor.\u003C/p>\u003Cp>Okay. Now that you use this helper, can we have it apply this automatically? Yes. But knowing Nuxt, we should automatically use this. Use personalization.\u003C/p>\u003Cp>There's our page content. There's the segment key. Does this still get us what we want? So far, looks like it does. Right?\u003C/p>\u003Cp>And the nice thing here is because we're using a cookie, and even though, like, this would be server side rendered, I'm still getting the content that we want. Right? Because it's automatically running a this cookie first before we actually display the content. Cool. Now let's do we have time?\u003C/p>\u003Cp>What kind of time do we have here? We got fifteen minutes left. Where do we take this further? Right? Where would we go from here?\u003C/p>\u003Cp>How do we set this up into, like, something like blocks, right, where we want to build a page out of the blocks? So for that, we can use the mini to any builder inside Directus. We've got this mini to any builder. We're gonna call this blocks. We don't have any related items yet.\u003C/p>\u003Cp>So first thing we're gonna do here is create a hero block. Call it block hero because I like to be obtuse. Alright. The hero is gonna have a headline. It's gonna have a description.\u003C/p>\u003Cp>Great. Okay. And then we have a block CTA. Cool. And that is going to have a call out and a button text.\u003C/p>\u003Cp>Great. Okay. Now we're gonna put those together with the many to any builder. These are gonna be our page blocks. We're gonna say block CTA, block hero.\u003C/p>\u003Cp>Great. Basically, this many to any relationship, it it doesn't necessarily exist, inside, like, standard SQL. Right? So Directus is doing a bit of API magic here. Basically, what what's happening, and we'll just hide, like, what we've got already.\u003C/p>\u003Cp>Alright. Hide this field. Don't destroy. Just hide. So we can see what's happening.\u003C/p>\u003Cp>We've created these separate collections, and then there's a junction collection here that is storing the page's ID and then we're storing the ID for the item. And then we also have a string for the collection that we're pulling this from. So it's, you know, basically some API magic to string these things together. But now if I go in and I update the page, we could see we've got a CTA that we'd say, this is you should try Directus. Do it now.\u003C/p>\u003Cp>Great. There's our CTA. There's our hero. Brian is cool. Well, he's not.\u003C/p>\u003Cp>There's our description. Alright. And let's hit save and stay. Let's just look at our page content now that we're getting back. Right.\u003C/p>\u003Cp>We're showing two different blocks here, but again, we need to go in and set our permissions. Directus keeps you secure by default. So we're gonna add just read permissions for all this. Again, for production, we wouldn't do this, but can we get to personalized blocks in twelve minutes or less? I need to get my ass in gear.\u003C/p>\u003Cp>Alright. Alright so we're gonna go back to our slug here. We're gonna change this up where we have our blocks. And actually we could use like a object syntax as well. Blocks, we're going to have here.\u003C/p>\u003Cp>We're going to get the we could get that. And then we're probably also going to have, like, variations within the blocks themselves. Well, this is getting messy. Right? Okay.\u003C/p>\u003Cp>So now let's just go back to the page. Variations blocks. I don't wanna do variations. We wanna do blocks. Alright.\u003C/p>\u003Cp>I'm gonna pull this back in. For our page render, we're just gonna have to pull that out. And now we could see we got, like, some data for each block. Right? Blocks, items, we're gonna wanna grab the item for each block.\u003C/p>\u003Cp>I don't need it. Fields comma. What do we got here? Okay. Well, yes, no, maybe so.\u003C/p>\u003Cp>Something is off. We got one too many. How nested did we go here? This one needs okay. Did we get it right this time?\u003C/p>\u003Cp>We still did not get this right. Direct as requests. AI is totally screwing this up, and I've had too much coffee to fix this properly. Alright. So we got the block.\u003C/p>\u003Cp>We're within the block, we're going to have, the items items, and then we're gonna grab the variations. Where okay. Backup. Backup. Backup.\u003C/p>\u003Cp>Love all the formatting. Items. Oh, yeah. This is not going well for me, is it? Fields.\u003C/p>\u003Cp>We're gonna grab the collection field. Great. Okay. Now, we got item. If I change this to item, we should see the item text.\u003C/p>\u003Cp>Great. And then also we're gonna have variations on that same item. Cool. Alright. But we don't have those yet.\u003C/p>\u003Cp>Alright. So let's go in and add those. So now the same way that I created that translation function on, let's say, the page level, we're gonna go through and we're gonna do this speed run style in less than eight minutes. We're gonna call this variations. Great.\u003C/p>\u003Cp>This is gonna be segments. This is the collection there. And what are we gonna call this? We're gonna call this the segments. Block hero ID, block hero variations.\u003C/p>\u003Cp>Great. Primary key is gonna be the or the primary indicator is gonna be that. We'll set this up. Alright, so now on our block hero I should be able to set up like a variation here except I don't have that content yet. So we'll go to our block CTA.\u003C/p>\u003Cp>Oh, no. Block hero. We're gonna copy that headline and description down into the next collection, right, to for our variations so that we can have that specific content. Block hero variations, duplicate. Great.\u003C/p>\u003Cp>Now, I go in, we've got our block hero I'm gonna stick up at the top. We have a headline for Brian's cool to editors. Brian is cool to developers. Great. Okay.\u003C/p>\u003Cp>And we're gonna create a hero component. View, create a hero component with Tailwind that has two props, headline and description. Let's see what it comes up with. Outsourcing the work here. Six minutes.\u003C/p>\u003Cp>You know, we could declare this one good, but, yeah, let's make it fun. Alright. We got the hero component. Okay. So now within the like this page section, I guess, Let's go and call that hero component with the page headline and description.\u003C/p>\u003Cp>Context switching sucks. Do we have a oh, no. We don't have that at the page level. Right? We're gonna loop through all of our blocks.\u003C/p>\u003Cp>So the, let's do template v four page blocks within the blocks. If the block collection equals the hero to block hero, basically, block underscore hero, that's the name of our collection, we're going to render that out. But now we also need to use the personalized page, page dot blocks, personalized page dot blocks. But low and behold, we're still not changing that content based on that because our personalization is what? At the page level.\u003C/p>\u003Cp>Right? So AI, help us in our moment of need. Five minutes left. Change this to well, accept a what a to accept content and recursively merge if there is a variations key within The array within the objects. There's a variations key.\u003C/p>\u003Cp>Date. And task. Objects. Let's see what this comes with back. What if just curious.\u003C/p>\u003Cp>Alright. Merge variations, current segment, matching variations. Is this to me, like, obviously highlights the dangers of AI? I am on a crunch here. Right?\u003C/p>\u003Cp>I don't have a clue. I don't have enough time to, like, figure out if this is actually going to do what I wanted to do anyway. So, you know, great that you can I I mean, I love the fact that I can just, like, quickly POC this, but, you know, it's like a what are we coming back with here? Personalized page. Let's just see what it comes back with.\u003C/p>\u003Cp>Blocks zero. So, again, like, is yeah. See, that's goofed up. Oh, and I'm not even giving the personalization content, though, am I? Why is that?\u003C/p>\u003Cp>I should be fetching it here. Items, personalizations. We gotta go back into our permissions, block hero variations, access. Save. Are we now at least getting that?\u003C/p>\u003Cp>Yes. Okay. So AI did save the day here, basically. You know, before I shipped any of this, I would need to go through and, like, study this in detail to figure out is this actually doing doing right. It appears to be doing right, but are there gonna be, like, dangerous side effects?\u003C/p>\u003Cp>But here I could see Brian is cool to developers. You know, maybe we go back in and within this, do we have a does don't like him? Alright. No. He's not.\u003C/p>\u003Cp>Neither do content editors. So there is that. You know, the one thing I noticed is we should have, like, a fallback and it should take that fallback into account. So it's not doing, like, a deep merge, but now we could see that content. We could clean this up just to show it out.\u003C/p>\u003Cp>And boom. So now we've got, like, page block level data where I could go through and build a page and have segments and personalize that data, for each individual segment. So I'm I'm gonna call that a win. Right? We got one minute fifty six seconds left.\u003C/p>\u003Cp>Would I have been able to achieve this without cursor? %. Would it have taken me more than an hour? One hundred percent. Is this still cool and fancy?\u003C/p>\u003Cp>Probably not. This is the start to something incredibly cool where, you know, I could go in and on the client side have some type of tracking and some type of rule engine that just, like, consistently con like, creates these different segments or groups of visitor into segments. And if you keep that on the client side, it could be privacy friendly as well. But I hope this was an interesting episode, an exciting episode for you. I've had a good time.\u003C/p>\u003Cp>I'm probably gonna figure out a way for us to leverage this somehow in the future. That's it for this episode of 100 apps, one hundred hours. Thanks for joining me. I do roll like the success message. See you guys next time.\u003C/p>","Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie, here from Directus. And today should be well, I think it's gonna be an exciting show. My confidence level on this one, just to put that out there, is fairly lower than normal. But today, we are going to be building a website personalization engine. I went through several different titles for this one. I don't know if that's actually gonna stick when this goes on Directus TV or not. But, what are we trying to do? Well, first, let's explain the rules. If you're new to 100 apps, one hundred hours, the rules are simple. Basically, we have sixty minutes to plan and build a clone, an application, a website. So that is plan and build, nothing more, nothing less. And then the only other rule, which is the I call it the anti rule, is use whatever you have at your disposal, which I will probably be leveraging, some AI today because I am wading into uncharted territory for me. So website personalization engine, sixty minutes. Let's do this together. Or it'll, like, publicly laugh at Bryant. Try. Alright. So we're gonna put sixty minutes on the clock, and let's cover this. Alright. So website personalization, you know, originally, this idea was for, like, personalized landing pages. Like, I know a specific company or target account that I want. Let's create a specific landing page for them. But in the light research that I did before this, which is basically, like, ten minutes of Google, basically, when it comes to personalization, it's more of a segment based approach. So that's what we're gonna sketch out here. You know, we wanna bucket visitors into a segment and then display certain content for that spec segment to personalize it. So, let's plan out our kinda data model. And, you know, I could leverage, like, some of our direct to CMS starters here. We're just gonna keep this stupid simple super simple. I tell my kids not to say stupid, so I probably shouldn't say that either. But alright. So as far as our data model, let's flesh out the application. Maybe we do just wanna, like, flesh out what are the actual features that we want out of this specific application and make that less huge. Alright. So we want to have a piece of content. Great. Directus will manage that for us pretty easily. And then we want to show variations of that content based on traits of a visitor or segments segments that a visitor belongs to. Now, something like this or or even when you get into, like, AB testing, you know, it's fairly easy. I won't well, I won't say fairly easy, but it's easy enough to set up, like, an AB testing code. If this, do that. If this if else, do that. But, like, the intersection of, like, handing this off to a content or a marketing team and setting it up in code and, like, managing all of that to work together for front end, back end, and making it cohesive, really difficult task. Right? So this is really all I'm trying to achieve today. We could make this fancier maybe down the the line, but let's let's discuss the data model for this. What are we gonna set up? So I could just call this content. Maybe it's, let's just have a pages collection. You know, this could be like a post or something like that. And then we're going to have what else? We're probably gonna have some segments. Like, here's the different segments that we want. And, you know, if you were doing, like, a proper system, you'd probably want something like events. Not super concerned with that. You know, the segments probably have, like, some rules for each segment or yeah. What are the rules? This sort of thing. I don't even know if we'll get this far. Let's just try to wade through this and and show something on the page. Alright. So let's get into what I've got set up. This is, just a Nuxt application. This is my standard starter for, you know, like, if I was just YOLO ing into a hackathon, this would be my starter. You could see a bunch of commented out code. But basically, I've got a blank Directus instance, if I can actually get logged into this bad boy. Directus is gonna be our CMS. It's gonna handle all of the back end for us. Dear lord. Why can I not get logged into this thing? Boy. Boy, oh, boy. Admin example. Password is the password. It's not even up, but that would be why that would be why I couldn't get into the application. Alright. So I've got Directus set up, in a Docker Compose, just a standard local setup kind of format. And like I said, Directus is gonna be our back end, our CMS. Okay. Great. Blank slate. Nothing fancy here. We do have templates available for stuff like CMS if if that's something that you're into. And then we have a Nuxt website. Alright. So let's start building some functionality. And inside the Nuxt website, we've got, like, an index page. So that's what we're staring at here. Alright, so let's add some pages content. Great. Go in, we'll create a new collection inside pages. For those of you who are new to Directus, what's happening behind the scenes here is Directus is connected to my Postgres database and it is basically gonna mirror whatever changes I make here to that SQL database. And I could connect like a an existing SQL database to this as well. So we have some pages. Great. Got a collection that's gonna show up here. When I hit new, there's nothing shows up. So we need to add some fields to our pages. And again, like, we're modeling our data here. We're generating an API and we're building a form for our content editors all in one place. So page needs a title. Great. A page needs a slug. So we're just gonna create a new field called slug. I'm gonna go into the interface settings and make sure this is URL safe. Do I have my little pointer? Yeah. I do. That's a cool little tool. Occasionally, I get questions on it. It is called mouse pose, Mac only, I think. Alright. So what else are we gonna have on our pages? We got a title. We got a slug. Maybe we'll keep this simple. Right? We've got a headline. That could be some text. Cool. And because I am totally gonna make it easy on myself, we're just gonna call this content. One big rich HTML rich text content block. This probably looks more akin to, like, a blog post than a page. Directus does have that amazing many to any like page builder scenario that's covered in some of our templates but we gotta crawl before we can walk. Alright so we got our pages, lets go ahead and let's just create a page. Right? This is Brian's page. Brian's page. Cool. Hey, yo. Why am I talking to myself? Great. Alright. So we got some content. Now if I want to query that via the API, I can go to the directus URL, go to items, pages, and boom. I could see I have a page here. There's my content. API is ready to go. This is already I've I've got the Nuxt application wired up with, like, a simple Directus SDK client. So we're creating Directus, we're using REST, and then we're providing that to the Nuxt application. So So if I go in and I do something like this, where we're just gonna create a new dynamic route inside Nuxt and do I have, like, a v t s? Yep. Script set up. Really need to work on my snippets to save time here. And my script at the top guy, when I'm working with Nuxt review. Alright. So now we want to fetch some of this page content. Right? Because what happens now, I've got this. If I go to Bryant's page, nothing shows. Yeah. That sucks. We need to resolve that. Alright. So what we're gonna do, we will do const can't type today. You'll notice I am using cursor here. Directus equals use Nuxt app. And, okay, we've got some auto completions that are not necessarily gonna give us what we want. Alright, so we're gonna get the data. Nuxt has the use async data function or the composable that we're going to use. If I can actually type that, great. We're gonna give this a key. So, the key, if we grab the routes, is going to be the slug, route. Params. Slug page. Let's just tag this as a literal. There we go. Thank you. Alright. And then we are going to return directus dot request, we're gonna do read items pages. Pages. K. And we're gonna filter where the slug, that's right, is equal to route dot params dot slug. K. So we have direct us there. We're gonna import read items from our SDK. You know, I could potentially, like, import that here and provide that as well if I wanted to, but we'll just do it this way, keep it nice and easy. And then let's see if we can just actually get the data. It'd been a minute since I messed with this. Is this actually going to work? Are we making a network call? Oh, wait. Use async data. There's that. There's the call that we're returning. Read items pages. Let's check our network requests. Are we seeing any actual network requests? It is server rendering these. So we are not seeing that. Probably want to add some error handling. So we'll just add that. If there's an error dot value, we are going to console dot error, error dot value. Great. Do I have any formatting set on this? Got a little bit of formatting. Alright. Can we see the error? H three error create error. What is going on there? There's an error. Error dot value dot message. What's the format for this? Create error. We're throwing an error. Hey. I'm guessing though, just from past experience, this is because we have not enabled any access to that page's collection. Right? So when I do this, I am still using the session token from Directus. But if I open this up in a new browser, we could see, hey. I don't have any permissions. So I'm just gonna enable permissions for pages. Let's do read permissions, see what we got. Boom. We can actually get some data and wrap this in a nice pre tag to see what we've got. Okay. Cool. Now let's take this data that we have and we'll add an h one. This will be the page dot headline. And then we are going to check and see what all I do have in this application. I don't have, like, a pros component. So we'll just do something like this where we have VHtml content. Now if I refresh if I refresh if I refresh, why is it that actually showing? Where's my content? Page dot headline. How is it that I could see the page content? Oh, I do have it here. Page dot headline. Why isn't that actually showing on the page, though? Interesting question. Interesting. Definitely. Let's just center this up. Maybe let's max auto, max width, four x up. Add some padding. Why isn't my content displaying via page? Do we need to wait on the page? Am I really not smart enough to figure this part of it out? Is there some type of error that we're getting paged out headline? I could clearly see oh, yes. That's why, Brian. You are getting an array here, so we need to transform that. So async data. I think it's here that we can do a transform. Data. Data. Just the first item. There we go. Alright. There we go. Hey, yo. Why am I talking to myself? Make sure that you understand what your data looks like before you start trying to mess around with it. We're gonna make that giant. And if we slap a pros tag onto this, pros l g. I think I've got Telen typography included here so we could see this data. Okay. So we're rendering something out to the page. Great. Now, this is super fancy. Right? Let's just grab something from the Directus blog to use as content. Here's a post that I wrote. I'm just gonna copy this entire thing. We're gonna go in and throw that here. Great. Just to get something on the page. Okay. We'll call this context switching sucks for devs. Cool. Alright. So now we are showing a basic thing on the page. Hallelujah this is amazing this is the coolest thing we've ever built. How are we doing on time? We got about forty five minutes left. All right so we have a piece of content now we want to show variations of that content for an actual visitor. So how are we going to achieve this sort of thing? And, you know, if we boil this down to the basics, we've got a page. We're going to have variations of page content. Yeah. We could call this personalizations or page variations. That's gonna be linked to our segments, and this is not right. I do have a PhD in drawing arrows, just not in Figma. This is still a challenge for me. Alright. So let's create this and I think what we can do is hijack some of the functionality that already exists in Directus for this sort of thing. Going off label here, off brand. So, we will go into our data model. What I'm gonna do, I'm just gonna create a page variations. Yeah. Naming stuff is probably the hardest challenge in development. You know, we could call this personalizations, but that might make somebody mad. Yeah. I'll just do created at, created by. I don't necessarily need this information. These are just helpers that you can add. Directus, like, prefix or, like, sets these up for you. So whenever, you save this item or create a new item, it obviously records the user, etcetera. Just some shortcuts. Alright. So we have page variations. We've got two different collections here. Great. How are we gonna tie these things together? Now, what I can do and what I'm gonna steal is our translations feature. So if we go to the docs actually, let's go to the new docs. These things are in beta as of now. We're gonna look for translations. Great. We have the ability to translate content and manage all those translations for you. And if I open this in a new tab, we get, like, this beautiful side by side interface where I could see, okay, here's English versus Spanish or French. And this is like, when we boil it down, like, when I think about it, I'm just now coming to me through my head here, is it basically, translation is a form of personalization anyway. So why not leverage this existing structure? You know, short of, like, the icons and maybe some of the other stuff that is baked in there, I I think it should work. So I'm also going to like, normally, if we were doing translations, you would create, like, a languages collection where you would store all the languages that you wanna translate your content into. In this case, what we're gonna do is just use, I'm gonna create a new collection. I'm gonna call it segments. You know, we could manually generate the IDs for these or we could use, like, a generated UUID. You know, if I was trying to scale this out, I would definitely be using, like, generated UUIDs, but let's just call this key we're gonna manually enter a string for this. So whenever we create a new segment, we're gonna manually enter in the key. That'll help us keep things a little bit clearer. Updated at updated by just to keep track. And do we want this segment to be active or not? You know, maybe we set that up on a a Boolean toggle. Alright. So we got a key for it. Maybe we want, like, a proper name. So we add a name field and drag this down. And what I'm doing here is basically just configuring the actual form that we're gonna use to set this stuff up. Do we have a segment? Do we have a category? Yep. There we go. Yeah. Again, naming things is hard. Let's add some icons. Just the designer OCD for me kicking in. Doc. Yep. There we go. Edit document. And I'm just gonna like actually hide these page variations. So now we have pages, we have segments. Let's create a new segment. Right, I want to speak to developers. We could say these are for developers, that's our nice name. Directus is also helpful for content editors, so I'll do content editors. Yeah. I wish I was doing this live so I could say, hey, hyphenate or underscore, but let's just go with a hyphen. Fine. Fine. Fine. You know, I could keep adding these as much as I want. Right? So now we've got some segments, we've got some pages. We wanna generate these variations. So again, we're gonna reach for the Directus translations interface. And I hope this works out like I think it will or like I hope it will. You can see that we're pre setting the languages collection. Directus is smart enough to know, like, when you're doing translations, you need languages, we can create that for you. But what I'm gonna do here, I'm gonna open up advanced field mode instead, and I'm gonna go in and I'm just gonna do all of this mapping myself. So we try to be smart and create junction collections and everything for you, but you can take total control of this as well. So here, this is the collection. So we're gonna call this page variations. I'm just gonna use page and we're gonna call this the segment and the collection that we're targeting is segments. So, I'm gonna set these all up to cascade, so if we delete a page variation or deselect one, we delete that content because we're not going to use these variations across different collections or pages. Alright, so the language indicator field. Now normally this is what shows here right up here. Right? Just the indicator of what this is. We could use the key. We could use the name. Let's use the name. Language direction. We're gonna ignore that. Default language is the primary key. Let's let's say I wanna default to developers. And we're gonna start this split. So we'll start open like this. Great. Okay. So now we create this, and we could show related values. You know, you can even preview the translation content. And I already see one thing that's going to bug the crap out of me as I called it translations. I forgot to rename this. Alright. So we're gonna call this variations. So it's not translations, it's variations. We're just piggybacking off of functionality. Alright. Pages. Cool. This is gonna be the what? Key? No. Segment. We're gonna call that segment. That's the segment. The collection is segment. And I gotta set up this nice cascade again. Okay. Go through the exact same process all over again. Love shooting myself in the foot Just to make sure we have variations. Is variations spelled correctly? At this point, we don't know. Alright. So I probably goofed something up already. Did I? Did I not? Page variations. Page variations. Page page is not found. Oh. Yep. Alright. So love it when I goof up. Cause more work for myself. We're just gonna start this process all over. Alright. Try it one more time. Page, variations, Generate the UID. I'm not even gonna go for that. At this point, we're gonna say translations interface. Great. This is gonna be segments. We're going to use the segment name. We're gonna start with the split open view. And let's control this. Right? Page variations. This is gonna be page. Segment. Delete. Alright. So definitely don't do what I have done. Let's try this. See if that works. Doesn't have field page and doesn't have a relationship. And what in the world field page in collections doesn't have a relationship? Why doesn't it it should have a relationship. Pages, segments, translations, start open. Why doesn't this have a relationship? Well, let's just see what happens when we do this in orally. Translations already has a relationship. Alright. So we can see here by adding this we get a languages and then we get a pages translations, there's a pages ID, there's an ID. This should work out okay. I don't know what is going on. Maybe I didn't refresh the data model. Key name, segments, page variations. Why am I not getting this to work correctly? Let's just take a look at our database and see what's happening. 53Edit. 5 3 2, test, Connect. Alright. So now we're behind the scenes into the database trying to figure out what I goofed up to begin with and it looks like there's some relationships here that got left over that didn't get fixed that are causing problems. Alright. So now with that out of the way, let's try this again. Page variations, we're gonna generate an ID for those. I'm just gonna skip all the fanciness. And now I'm gonna go in and do translations. We're gonna call this variations. Instead of languages we're going to use segments, we're going to use the name of that. We are then going to set up to use page variations, page segments, segment. Okay. Now is that going to work out how we want fingers crossed. Let's log in and see what have we got. Okay. So now I get this side by side interface of content editors and developers. Right? But I don't see anything inside the actual form. So inside this junction table is where we're gonna go in and create our content that is actually going to vary. So in this case, it's just basically going to be our headline and our content. And I can quickly duplicate these. So we'll go to page variations. I'm gonna say copy headline. We're gonna copy the content. Just make sure you change that copy that we prepend at the end so that you don't, you know, get confused. But with this information set up, now I can get an interface that looks like this where we can say, hey. Great. Now we've got developers. We've got content editors. Cool. And I can be able to store different contexts here. Context switching sucks. Great. That's what we'll change the default headline to. We're gonna change this for devs, Context switching sucks for content editors. This content is for editors. This content is for devs. Alright. So now thinking through our mental model, we've got our default content. And then if we identify a segment, like if the visitor falls into the content editor segment or they fall into the developer segment, we're going to show them different content. Now, what is this actually going to look like? So if I go and like we look at the API now, got my API request, you could see those variations. One of the beauty, beautiful things of Directus is the ability to use the rest API in a GraphQL like manner. So I could do something like this where I just say, hey. Give me all of the root level fields and also give me all the fields within our variations. So now I can see here's my variations. The segment here is developers. The, segment here is content editors and presumably on the Nuxt side of it. And, you know, I could rig something up via the API to do this swap for me, but depending on how I'm rendering my site, you know, let's say I'm statically generating the site, if I want to still offer this, I've got to pre generate the payload, you know, pre generate our JSON and store that so I can use it when rendering it. So we're gonna try to do this on the client side. Inside our page level data here, right, we're still getting the same result. And if I add the data back, you know, now we should be seeing our variations, but hey, lo and behold we're not because again we need to go back into our access policies and set that up. So we're going to allow read access for our page variations and for our segments just so we could see those things. You know, when we're going to production, we'd probably make sure there's some kind of status on these so that, you know we're only showing published pages etcetera, but outside the scope. So now I can see my variations. We're gonna go here and let's just specify our fields, right. It's not content, it is variations of this content. And now we could see that. Great. Alright. So now we need to translate this content or personalize it in this case. Right? How do we assign a segment? You know, like, writing a rules engine here would be, kinda challenging with the twenty seven minutes we have left, I would say. But, you know, at the core, we we've gotta have some way to assign the segment. What I'm going to do I I think we're just gonna use a cookie, for this. So Nuxt has a use cookie composable. Let's just call this the segment key. We're gonna use cookie or you call that segment. Great. And I'm just gonna make this stupid simple. We'll add a div. We'll flex these, add some gap, make sure that's in the middle, etcetera. And then we're gonna add some buttons. I've got the Nuxt UI library in here. Segment key equals developers. No. We're not gonna do designers, but, we're gonna do content editors. Content editor. And then maybe we just add I am a great. Alright. Maybe we wrap this whole thing in. I think it's a u container from the Nuxt UI library. Okay. Yep. So we get a little bit of spacing there, And we can add, just some space between these space y eight. Okay. Great. And this is probably all part of the same section. Okay. So I am a developer. I am a content editor. I am a developer's. Great. Again, naming is crucial here. But basically, like, what I should be able to see now if I go into, like, my cookie storage, I can see that segment as well. Right? So based on this, I'm tagging that person. You know, and we're just explicitly asking. But, you know, we could set up, like, some kind of client side rule engine or, you know, you could potentially do this on the server side as well. Whatever pages you visit, weight those against each other and assign a, you know, if I've got five articles for developers and you look at three of those, then I'm probably safe to say that you're a developer, versus, you know, reading the content editor pages. Alright. So how do we actually translate this? Right? We probably want, like, a helper function that takes our page data and then evaluates the segment and then, you know, spits out the proper content. So let's just see how awesome AI has become here and see if this is actually worthwhile or I should just probably write it out. But, this wouldn't be fun if we weren't using something like cursor and, you know, something super opinionated or where everybody has an opinion about AI at this moment. Here we go. Write a let's just describe this. Right? Personalize the page content based on the segment key. Where is this gonna come up with? Personalize content, page value dot variations. I okay. You know, maybe this would come back. Let's see. Helper function to get the personalized content. Page variations return page dot that's not really content. Right? We just wanna return yeah. Okay. So I'm probably not prompting right. Let's see what we can do. Take in the page and the segment key and return a merged page with the proper variation based on the segment key. That's better, still not great. Right? You would probably want, like, a personalized function where it, just basically takes in, the page content and the different segments and the current segment and returns that. But nevertheless we got our personalized page or we should, right. Great. We can see our variations, we can see the content. If I check-in view dev tools, we go to our slug page. Do we have the personalized page? Great. That doesn't really Context switching sucks. No. But that doesn't look right to me. Right? So we do personalized page dot headline, personalized page dot content. Context switching sucks. I am currently in the developer side of things, so it should be showing should be toggling this. Right? So there's something wrong here. Page dot value dot variation. Yeah. So, basically, what we wanna do is loop through the trying to to do this the AI way and not get frustrated. Loop through the page variations and merge them with the page based on the segment key. Still showing the same thing. Personalized page. Yeah. Okay. Yeah. Alright. So this is the final page. Let's define that. The final page. Okay. Then we are going to is this getting smarter now? For constant of variations, if segment dot key, final page, return final page. What does that do? Why is it why is it still not doing what we want? Page.valuepage.value.variations.developers. Oh, yeah. That's why. Basically, we need to use, like, a file. Constant variations equals variations. Selected Constant variation is going to be we basically need to look through this array. Variations dot find. We'll look for the variation. And there we go. Okay. Yeah. So we're getting that array. That's the problem. So now if I remove this blah blah blah page, Now I have personalized content for folks. Amazing. This is the personalized content engine, website personalization engine. Badabing badaboom. This is beautiful. Right? So now I can define all of these variations within our actual CMS. Like here's the segments that we want to create content for. You know, I could potentially set up some type of rules engine for this. And then, you know, on our individual pages, then I can go through and define this content. Now is this a real world scenario? Probably not. You would take this and use something like our mini to any builder where you're building these dynamic pages and within each section you have personalized content for that. But at the very least, hey, this is now we've got, a pretty robust I won't say robust. This is not robust at all. But it is a a pretty it's a start to a personalization engine basically. That's it. That's all. Now where do we go from here? Right? I this would be extracted out into a, like, a helper function, so that, like, each page or each block, we would we would call this function, return the content if there is a variation, like, if we have a segment key. Yeah. And we could even go as far as, like, creating a rule engine for this. So let's just continue to beat on AI, which is, honestly not been great so far for this. Let's say extract this out to out to a helper function that is more robust, maybe. I don't know. Let's see. I'm gonna take a sip of coffee while it's trying to choke that down. The auto completions are not great, but here we go. Let's see what it's come up with. Personalization. This looks to be a composable. Use personalized page. Here's the page. Segment key. Update the page component. Here's how we're gonna use that. Personalize page. Use a page. There is a segment key. Okay. AI. I will bite. Alright. App utils. Personalization. And I don't know why it's using the is there let's see, like, an actual it's using the composable format for view, but import type maybe rest. Return computed. I mean, to me, this is a I guess this is a this is a composable. I'm not sure why it's doing that. So we'll stick that there. This is use personalization. This is the name of it. Use personalization. Again, probably not a strong choice for, like a composable here because this is probably something that you might want to extract out and use, like, across different projects. Use personalization. Use personalization. Okay. Cursor. Okay. Now that you use this helper, can we have it apply this automatically? Yes. But knowing Nuxt, we should automatically use this. Use personalization. There's our page content. There's the segment key. Does this still get us what we want? So far, looks like it does. Right? And the nice thing here is because we're using a cookie, and even though, like, this would be server side rendered, I'm still getting the content that we want. Right? Because it's automatically running a this cookie first before we actually display the content. Cool. Now let's do we have time? What kind of time do we have here? We got fifteen minutes left. Where do we take this further? Right? Where would we go from here? How do we set this up into, like, something like blocks, right, where we want to build a page out of the blocks? So for that, we can use the mini to any builder inside Directus. We've got this mini to any builder. We're gonna call this blocks. We don't have any related items yet. So first thing we're gonna do here is create a hero block. Call it block hero because I like to be obtuse. Alright. The hero is gonna have a headline. It's gonna have a description. Great. Okay. And then we have a block CTA. Cool. And that is going to have a call out and a button text. Great. Okay. Now we're gonna put those together with the many to any builder. These are gonna be our page blocks. We're gonna say block CTA, block hero. Great. Basically, this many to any relationship, it it doesn't necessarily exist, inside, like, standard SQL. Right? So Directus is doing a bit of API magic here. Basically, what what's happening, and we'll just hide, like, what we've got already. Alright. Hide this field. Don't destroy. Just hide. So we can see what's happening. We've created these separate collections, and then there's a junction collection here that is storing the page's ID and then we're storing the ID for the item. And then we also have a string for the collection that we're pulling this from. So it's, you know, basically some API magic to string these things together. But now if I go in and I update the page, we could see we've got a CTA that we'd say, this is you should try Directus. Do it now. Great. There's our CTA. There's our hero. Brian is cool. Well, he's not. There's our description. Alright. And let's hit save and stay. Let's just look at our page content now that we're getting back. Right. We're showing two different blocks here, but again, we need to go in and set our permissions. Directus keeps you secure by default. So we're gonna add just read permissions for all this. Again, for production, we wouldn't do this, but can we get to personalized blocks in twelve minutes or less? I need to get my ass in gear. Alright. Alright so we're gonna go back to our slug here. We're gonna change this up where we have our blocks. And actually we could use like a object syntax as well. Blocks, we're going to have here. We're going to get the we could get that. And then we're probably also going to have, like, variations within the blocks themselves. Well, this is getting messy. Right? Okay. So now let's just go back to the page. Variations blocks. I don't wanna do variations. We wanna do blocks. Alright. I'm gonna pull this back in. For our page render, we're just gonna have to pull that out. And now we could see we got, like, some data for each block. Right? Blocks, items, we're gonna wanna grab the item for each block. I don't need it. Fields comma. What do we got here? Okay. Well, yes, no, maybe so. Something is off. We got one too many. How nested did we go here? This one needs okay. Did we get it right this time? We still did not get this right. Direct as requests. AI is totally screwing this up, and I've had too much coffee to fix this properly. Alright. So we got the block. We're within the block, we're going to have, the items items, and then we're gonna grab the variations. Where okay. Backup. Backup. Backup. Love all the formatting. Items. Oh, yeah. This is not going well for me, is it? Fields. We're gonna grab the collection field. Great. Okay. Now, we got item. If I change this to item, we should see the item text. Great. And then also we're gonna have variations on that same item. Cool. Alright. But we don't have those yet. Alright. So let's go in and add those. So now the same way that I created that translation function on, let's say, the page level, we're gonna go through and we're gonna do this speed run style in less than eight minutes. We're gonna call this variations. Great. This is gonna be segments. This is the collection there. And what are we gonna call this? We're gonna call this the segments. Block hero ID, block hero variations. Great. Primary key is gonna be the or the primary indicator is gonna be that. We'll set this up. Alright, so now on our block hero I should be able to set up like a variation here except I don't have that content yet. So we'll go to our block CTA. Oh, no. Block hero. We're gonna copy that headline and description down into the next collection, right, to for our variations so that we can have that specific content. Block hero variations, duplicate. Great. Now, I go in, we've got our block hero I'm gonna stick up at the top. We have a headline for Brian's cool to editors. Brian is cool to developers. Great. Okay. And we're gonna create a hero component. View, create a hero component with Tailwind that has two props, headline and description. Let's see what it comes up with. Outsourcing the work here. Six minutes. You know, we could declare this one good, but, yeah, let's make it fun. Alright. We got the hero component. Okay. So now within the like this page section, I guess, Let's go and call that hero component with the page headline and description. Context switching sucks. Do we have a oh, no. We don't have that at the page level. Right? We're gonna loop through all of our blocks. So the, let's do template v four page blocks within the blocks. If the block collection equals the hero to block hero, basically, block underscore hero, that's the name of our collection, we're going to render that out. But now we also need to use the personalized page, page dot blocks, personalized page dot blocks. But low and behold, we're still not changing that content based on that because our personalization is what? At the page level. Right? So AI, help us in our moment of need. Five minutes left. Change this to well, accept a what a to accept content and recursively merge if there is a variations key within The array within the objects. There's a variations key. Date. And task. Objects. Let's see what this comes with back. What if just curious. Alright. Merge variations, current segment, matching variations. Is this to me, like, obviously highlights the dangers of AI? I am on a crunch here. Right? I don't have a clue. I don't have enough time to, like, figure out if this is actually going to do what I wanted to do anyway. So, you know, great that you can I I mean, I love the fact that I can just, like, quickly POC this, but, you know, it's like a what are we coming back with here? Personalized page. Let's just see what it comes back with. Blocks zero. So, again, like, is yeah. See, that's goofed up. Oh, and I'm not even giving the personalization content, though, am I? Why is that? I should be fetching it here. Items, personalizations. We gotta go back into our permissions, block hero variations, access. Save. Are we now at least getting that? Yes. Okay. So AI did save the day here, basically. You know, before I shipped any of this, I would need to go through and, like, study this in detail to figure out is this actually doing doing right. It appears to be doing right, but are there gonna be, like, dangerous side effects? But here I could see Brian is cool to developers. You know, maybe we go back in and within this, do we have a does don't like him? Alright. No. He's not. Neither do content editors. So there is that. You know, the one thing I noticed is we should have, like, a fallback and it should take that fallback into account. So it's not doing, like, a deep merge, but now we could see that content. We could clean this up just to show it out. And boom. So now we've got, like, page block level data where I could go through and build a page and have segments and personalize that data, for each individual segment. So I'm I'm gonna call that a win. Right? We got one minute fifty six seconds left. Would I have been able to achieve this without cursor? %. Would it have taken me more than an hour? One hundred percent. Is this still cool and fancy? Probably not. This is the start to something incredibly cool where, you know, I could go in and on the client side have some type of tracking and some type of rule engine that just, like, consistently con like, creates these different segments or groups of visitor into segments. And if you keep that on the client side, it could be privacy friendly as well. But I hope this was an interesting episode, an exciting episode for you. I've had a good time. I'm probably gonna figure out a way for us to leverage this somehow in the future. That's it for this episode of 100 apps, one hundred hours. Thanks for joining me. I do roll like the success message. See you guys next time.","7fed7f62-8460-4a82-b086-4943b7f0d6fa",[649],"9118d9ce-5497-44cf-a504-95bf6a70dd9d",[],{"id":142,"number":143,"show":122,"year":144,"episodes":652},[146,147,148,149,150,151,152,153,154,155],{"id":153,"slug":654,"vimeo_id":655,"description":656,"tile":657,"length":192,"resources":8,"people":8,"episode_number":323,"published":556,"title":658,"video_transcript_html":659,"video_transcript_text":660,"content":8,"seo":661,"status":130,"episode_people":662,"recommendations":664,"season":665},"company-intranet","1059437289","Bryant takes on the challenge of building a company intranet portal for fictional company \"Initech\" (complete with Office Space branding). Watch as he implements Google single sign-on, creates team structures with reporting hierarchies, sets up permission-based content access, and designs an announcement system. Follow along as he transforms Directus into a fully-functional corporate portal where employees see only what they're supposed to—TPS reports and all.","d63b665c-8bce-4c54-99fe-2783f1b44a56","Mission: Company Intranet","\u003Cp>Speaker 0: Welcome back to yet another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. This is the show where we try to build or rebuild some of your favorite apps in one hour or less or get publicly shamed trying. Alright. So if you're new here, there are only two rules.\u003C/p>\u003Cp>Sixty minutes to plan and build, which always results in fun and chaos. And the second rule, use whatever you have at your disposal, AI tools, past projects, random packages off of the Internet, whatever. It's fair game. Alright. Let's dive in.\u003C/p>\u003Cp>Today, we are going to be building a company intranet portal thing. Listen. This was honestly a suggestion from, some of the AEs on our team. Love those guys. They're amazing.\u003C/p>\u003Cp>The spec for this, very fuzzy. Right? I I don't even know what is in a company Internet portal sort of thing these days. I do know excuse me. I do know that I don't think I ever enjoyed using one.\u003C/p>\u003Cp>So we're gonna try to create something in an hour or less that is helpful and actually decent to use. Let's dive in and hit the clock. Alright. Sixty minutes. Usually, let's kick this off with functionality.\u003C/p>\u003Cp>Right? What type of functionality do we want out of an intranet portal? And we're gonna do single sign on for sure. Right? If we're using something internal, we want single sign on maybe through Google or Microsoft or something like that.\u003C/p>\u003Cp>We're gonna set that up. Yeah. We wanna be able to track team members and show org structure, you know, maybe provide helpful links. I feel like maybe that could be like a Google Doc, but, you know, we're we're doing the fancy version of this. And then what else do we're gonna have?\u003C/p>\u003Cp>You know, maybe I wanna be able to send messages to entire team. Great. Okay. So that is what we're looking at as far as functionality. Let's kind of discuss our data model.\u003C/p>\u003Cp>Right? We're going to need honestly, there's new shapes there. Figma, come on now. Okay. I don't I don't need a tour on the new shapes.\u003C/p>\u003Cp>Thank you, though. Alright. Getting derailed by the tools already. We're, like, two minutes into this. Alright.\u003C/p>\u003Cp>So, obviously, we are using Directus on the back end here. That's gonna give me a lot out of the box, like, user permissions, like users. Right? So we're gonna have users of the application. Those are also gonna double as team members.\u003C/p>\u003Cp>So one shot, one kill here. You know, users comes with emails, passwords, first name, last name, job title. We're gonna probably want to add, like, reporting structure, direct reports, reports to, something like that so I can establish those relationships. We probably want, like, a team. So we got different teams within the organization, so that'll be a separate one.\u003C/p>\u003Cp>Why is that text so small? Oh, too big. Too big. Okay. Alright.\u003C/p>\u003Cp>So we got teams. We got users. What else do we have? You know, maybe, like, pages or resources. I guess really fuzzy.\u003C/p>\u003Cp>Right? And this is the part of the show where I draw these amazing arrows that that FigJam allows for. So k. Looks great. You know?\u003C/p>\u003Cp>And then maybe there's a relationship here where some team members have access to certain pages and some don't, that sort of thing. Let's see what we can get done. Right? Alright. So what have I got set up already?\u003C/p>\u003Cp>Basically, absolutely nothing. I've got a example project or a a Docker Directus instance running here. I've got, you know, just kind of like a standard Docker Compose setup. Don't have much config in this. No collections setup whatsoever.\u003C/p>\u003Cp>Alright. So let's tackle first thing on the list. Right? I want to set up single sign on. So if I log out of this instance, I see email and password, log in.\u003C/p>\u003Cp>I wanna do single sign on for, like, Google, for instance. Alright. So how are we going to configure that? Well, inside our documentation, we've got a list of, like, the configuration that we could set up. So we're gonna have, like, an auth providers.\u003C/p>\u003Cp>So that's a comma comma separated list of auth providers. The driver that we're gonna use and then a provider mode, we'll probably use session. And then for Google, I think the recommendation from our team is to use OpenID. So we've got, like, an OpenID setup here. Now do we have, like, a Google SSO single sign on?\u003C/p>\u003Cp>This this is the single sign on dock. Do we have a, like, standard config? Somewhere where we could see, like, a standard config for this. Yeah. There we go.\u003C/p>\u003Cp>Alright. So we could set up multiple auth providers, but this looks like the the pretty standard config for, Google. So I'm just gonna copy this wholesale. Right? I'm gonna go inside my Docker Compose, you know, or I could stick this in my ENV file if I wanted to.\u003C/p>\u003Cp>I'm not gonna open that because I've got some other secrets in there. As far as the IDE I'm using, this is Cursor. I've been using that this entire season. I found it pretty helpful and especially for quick POCs like this. Alright.\u003C/p>\u003Cp>So now we got some config in here. We're gonna have to go and create a client inside Google as well. So let's go to Google Google SSO single sign on, console. Cloud. Google, I think, is the let's see what we got here.\u003C/p>\u003Cp>Log in as Directus. Alright. Let's get started configuring. I've got my first project, so you're probably gonna have to create a project if you don't have one. Let's get started.\u003C/p>\u003Cp>We're gonna call this 100 apps, one hundred hours. The user support email, that's gonna be me, Brian at Directus. This will be an internal audience. So, my knowledge of Google auth is very minimal, But, obviously, internal is gonna be users only within our organization, which for this works well. Right?\u003C/p>\u003Cp>When I'm at directus dot I o. Great. I agree. We're going to spin this up. Okay.\u003C/p>\u003Cp>Nice. Nice. Okay. We're gonna create an OAuth client. This is a web application.\u003C/p>\u003Cp>Let's call this Directus. Alright. So what do we need to do as far as, like, our JavaScript origins and our authorized redirect URLs? So I think we're gonna have to do something like this where we have local host 8055 as far as our origin. Let's see if we can search our doc single sign on.\u003C/p>\u003Cp>Here's a entire guide on this. So you could just search that up. Right? Internal authorized domains. Alright.\u003C/p>\u003Cp>So we're gonna authorize JavaScript origins. That's gonna be it's not required, but that's gonna be our directus server address. And then the authorized callback is gonna be slash auth slash login slash Google slash callback. And that's because of the auth provider key that we're providing to the auth provider is Google. Alright?\u003C/p>\u003Cp>So that's how we reference this in our actual config. Alright. So, let's add that. That'll be HTTP local host 8 0 5 5. Great.\u003C/p>\u003Cp>We're gonna hit create. I certainly hope it doesn't take as long as what it's saying. Alright. So if I expand this, over here we're gonna get a client ID. We're gonna copy this client ID.\u003C/p>\u003Cp>That's gonna go here. Great. Then we got a secret. Don't steal my secrets. We're gonna paste that here.\u003C/p>\u003Cp>And what else do we need? Let's add an auth Google mode. Google mode. We're gonna make that session. The identifier key is email.\u003C/p>\u003Cp>I'm also gonna do allow public registration. Google allow public registration. So we want anybody who comes through our organization to be able to register for this. And one of the other things that we could do, I'm just gonna copy and paste. We probably wanna add, like, a default role setup as well.\u003C/p>\u003Cp>We'll get to that in a moment. Default role ID. Let's just actually dive into that right now. Alright. So great.\u003C/p>\u003Cp>We are going to sign in. If you remember the admin password, that is okay. So now we got this. Let's go in and create we've got two roles by default. These are out of the box for Directus.\u003C/p>\u003Cp>We've got public. This role controls, you know, like, what is available without authentication. And there's also a public access policy. So the access policies are, you know, just groups of different permissions, and then you can add many different access to a role. I love the granularity here, and it's super easy to maintain, like, a complex permission system between the both of these.\u003C/p>\u003Cp>What we do wanna do here is basically let's create a new role. We're gonna call this a team member role. Great. Okay. Alright.\u003C/p>\u003Cp>So we'll just save that. I'm gonna grab the primary key for this. That's what I'm gonna stick here for the role ID. That's the role that we want folks to have. As far as the policies that we're gonna give them, for now, let's just give them full admin access.\u003C/p>\u003Cp>They could change anything. We're gonna scope that down eventually, though. Alright. So the next thing I'm gonna do to test this out, right, I want to I I just want to stop my container. I'm gonna spin that container back up, and I'm gonna open this in.\u003C/p>\u003Cp>Let's do an incognito window with the admin login. Admin and example. Password. Great. K.\u003C/p>\u003Cp>And now if I log out of this one because I'm I'm already signed in to Google for everything here. Alright. If I hit bryant at directus, create a strong password. Why do I have to create a strong password? Come on, dawg.\u003C/p>\u003Cp>Why is it asking me to do this? Use 20 characters or more. Come on, guys. What is it? What are we doing?\u003C/p>\u003Cp>I'm gonna do this off screen here. I don't I don't know why this is happening here. Google. Let's create a new password. 20 characters or more.\u003C/p>\u003Cp>Directus dot I o. Okay. Oh my gosh. Let's see if this is actually gonna work. Sign in to 100 apps, one hundred hours.\u003C/p>\u003Cp>The request cannot be processed because it is malformed, which is not good. Let's try it again. What are we doing wrong here? So we go back to our Google where's our Google application? Yeah.\u003C/p>\u003Cp>Sign in. Yes. It's me. Love it when all of this nice security tech gets in our way. What are we missing?\u003C/p>\u003Cp>Right? HTTP local host 8055 slash login slash google/callback. Okay. See, this is nice because I've tried to log in to a different account, but I should be able to log in to there we go. So now I am in.\u003C/p>\u003Cp>Right? And we can see that Bryant Gillespie is created. This is this is really nice. Right? We could see the auth provider here is Google.\u003C/p>\u003Cp>There's the external modifier or identifier, not modifier. Now we've got single sign on. Right? And one of the other things that I could potentially do I think there's config for this if we take a look. Go into auth and SSO.\u003C/p>\u003Cp>I could disable the default provider as well if I wanted to so that you could only log in with single sign on. So if we do that, let's see what we've got. Auth, disable default, true. And then I'm gonna restart my container. Let's just log back out.\u003C/p>\u003Cp>And now there's only Google to log in. There is no email and password auth options. This will disable it at the API level as well. Super nice thing to do. Right?\u003C/p>\u003Cp>If if all you want to allow is single sign on. So, with that done, right, let's let's go in and brand this thing because we obviously need a nice brand. Huge fan of office space. If you're younger and you're watching this, you know, this is an older movie. It pains me to say that.\u003C/p>\u003Cp>But, let's use Inatec as the company here. So I'm gonna go into our appearance settings, the project logo. So I could just copy the image address, take full advantage of this a lot of times. So I'll just import the file from a URL. If I hit save, we could see that Inatec logo up there.\u003C/p>\u003Cp>I'm gonna use the color picker. Let's pick up, like, a Inateq blue, maybe, I guess we could call that. Right? We got, like, this pale blue color. I'm gonna set up the Directus default theme, and in the namesake of the show, I'm gonna steal from our simple CMS template.\u003C/p>\u003Cp>I've got, like, a custom theme that I really like, super happy with. So I'm just gonna steal that paste raw value. There we go. We might wanna swap this out. And one of the nice things about the theming in Directus is, you know, I've got all these nice keys for theming, but I also have the ability just to add custom CSS and do whatever I want.\u003C/p>\u003Cp>So in this case, we'll do that as well. And what else are we gonna do? I'm gonna fix this just because it's gonna aggravate me. Alright. So this is where that CSS comes in.\u003C/p>\u003Cp>I've got the module bar logo. I'm gonna do something like this where we have dot module bar logo, background. Let's just do white. And we're gonna stick important on that. There we go.\u003C/p>\u003Cp>Now we're cooking. And maybe we wanna add, like, a subtle subtle border there. Let's see. What is that gonna be? That'll be our navigation.\u003C/p>\u003Cp>That'll be the modules. That'll be the border color, which would be I don't know. Let let's use color mix in sRGB. We'll use the var theme. We're gonna use the primary color and maybe 20% white.\u003C/p>\u003Cp>F f f f. And let's set, like, a two pixel border just so we can see it. Alright. There we go. So now we got a little little border action to keep things separated.\u003C/p>\u003Cp>I mean, this is not like a super nice mouse over active color, but I'm not gonna get too far into it. Right? This is gonna be the Inatec company portal. And one of the things that's gonna aggravate me is just our font choice here. Like, DM Sans is a newer font.\u003C/p>\u003Cp>Maybe we just set this to Arial. Do I have that installed? Would it be Arial? Yeah. I think it is one way or the other.\u003C/p>\u003Cp>And then we can set, like, our, let's let's pick something, like, super corporate here. Yeah, Roboto feels pretty corporate to me at this point. We'll do Roboto. So if I just put this in quotation marks, this will go ahead and pull those font files from Google for you. So any Google font, good, bad, ugly, whatever you wanna do, you can include those when you're customizing these themes.\u003C/p>\u003Cp>Alright. So now we got Inatek. We've got Roboto. This feels very robotic. This is a sad company worked for fictionally.\u003C/p>\u003Cp>So now if I refresh our login screen, we've got this kind of action going on. Looks okay. Maybe we wanna spice this up a bit. Right? So we're just gonna put Bill Lemberg on the login page.\u003C/p>\u003Cp>So we'll go to appearance. The public background here, we're gonna import Lemberg. Yeah. We need those TPS reports, and, you know, I will just leave the Favicon blank. There it is.\u003C/p>\u003Cp>There we go. Right? So this is the it's beginning to shape up as a perfect company portal. Alright. And one thing I'm gonna do, I'm gonna turn this off, or we're not gonna be able to test, like, our permissions.\u003C/p>\u003Cp>So disable default. Let me just spin this back up so we can get, like, an email and password. Log in one more time. Sign out. Okay.\u003C/p>\u003Cp>So now I've got my email and password back. Let's dive into the next item on our list, single not single sign on. We just completed that one. We're gonna dive into tracking team members and showing, like, an org structure, providing helpful links, and maybe try to send messages to the entire team. Alright.\u003C/p>\u003Cp>So, we've already got users through Directus. Right? We've got our user library here. We could see that Brian logged in, etcetera, blah blah blah. Looking great.\u003C/p>\u003Cp>Let's add this team functionality. Right? And we probably want the ability to create new teams and adjust teams and etcetera. So let's just create a collection for this. I could also just adjust the users collection, which we'll do in a few.\u003C/p>\u003Cp>But for this case, I'm gonna create teams. So each team, we need a primary key field. I could use an auto incremented integer. We can use a manually entered string. You know, I mean, like, in this case, maybe we use a manually entered string because I'm not expecting to have a a ton of teams, and, you know, the way we reference those is probably not gonna change much.\u003C/p>\u003Cp>Alright. Do we need any of these default fields? I'm not really sure. I don't think so. So we'll just skip that.\u003C/p>\u003Cp>So we got a team. We're gonna give a team name. Pretty simple. We'll make sure that is required. Great.\u003C/p>\u003Cp>The ID, we can make sure this is like URL safe, so we'll just enable Slugify through the interface settings. And we've got a team, we've got a name. Now what we're gonna do is set up a relationship between we probably got, like, a a parent and child relationship between teams as well. Right? One team could roll up to another team, could roll up to etcetera.\u003C/p>\u003Cp>You know, we could classify things like a team lead, and then we're gonna have, like, an organizational structure where we have direct reports for each user as well. So let's set up that first one. Right? Each team needs a leader. We'll just call this team lead.\u003C/p>\u003Cp>So here, I'm using the direct us relationships. I'm gonna set up a many to one because I only want one leader within the team. Could certainly set up multiple leaders if I wanted to, but for the related collection here, I'm gonna use direct us users. And as soon as I hit on the right one, you'll see that kind of turn blue and lock in. I can also just go over here and hit the system collections and do it that way as well.\u003C/p>\u003Cp>We're gonna show a link, and I'm gonna open up Advanced Field Creation Mode just so we could see this. What I'm also gonna do here, I can create the corresponding relationship at the same time. And this is where data modeling becomes very important. Right? Because, do we necessarily want this relationship here?\u003C/p>\u003Cp>Because I could be leading, you know, I guess I could lead multiple teams. So we will add this relationship here, but, you know, if you need, like, multiple team leads, obviously, you're looking at, like, many to many. We're gonna display the direct as user, show the user in a circle. We'll set that up. So there's the team lead.\u003C/p>\u003Cp>And now if I were to go and check our data model for users, right, I should also have a Teams option here. Right? There's our Teams. So we can specify the do I actually want that? The we're gonna have Teams as another thing, like, teams that we're a part of.\u003C/p>\u003Cp>Let's delete this, recreate this relationship, one to many, and teams leading. It's not t lead teams leading. Yeah. Leads, teams, teams leading, I guess. Naming stuff is I say this on every episode, I feel like, but naming stuff is the hardest thing in programming without a doubt.\u003C/p>\u003Cp>Alright. There we go. We got the display table, and we're gonna show the name. Cool. Team lead already has an associated relationship.\u003C/p>\u003Cp>What are we doing here? Have I bricked this whole thing? Let's see what we've got. Can I get access to the relationships here? If I reload what is going on?\u003C/p>\u003Cp>Have I totally bricked Directus with this relationship? What have I done? People, what have I done? Permissions, policies, fields, direct us users, team leads. Now if I refresh, did this sort it out?\u003C/p>\u003Cp>What have I done? Alright. Teams. Team lead. I don't understand.\u003C/p>\u003Cp>Teams users leading does not exist. Let's just try down and up. I goofed up our relationship somehow. I think we can get this back. We can solve this problem.\u003C/p>\u003Cp>Now I would be remiss not to take this opportunity to talk about the data mirroring inside Directus. So, you can install Directus onto any existing SQL database, whether that's Postgres, SQLite, MySQL. Directus, like, bootstraps these name space collections as you see here, but everything else is just standard SQL. Right? There's nothing direct as specific about our team teams collection here.\u003C/p>\u003Cp>And as I go through and build these out, let's try this one more time. One to many relationship or actually a many to one here. We're gonna have a team lead. We'll pick our direct to users collection, and then in the relationship settings, I'm gonna add that one, teams leading. I hate that, but that's what we're rolling with for now.\u003C/p>\u003Cp>We're gonna create this relationship. Right? Now, we could see that that change is being mirrored here. There's my team lead. Amazing.\u003C/p>\u003Cp>Right? So I could go in. We could create a new team. We could do team America. Let's just call it bride's team or let's do marketing, I guess.\u003C/p>\u003Cp>Marketing. Great. Mister admin user can be our team lead, and then I can go in and add additional teams. Right? Product.\u003C/p>\u003Cp>Got the product team. Sales. Great. You know, we could get really fancy and add icons for all these teams. Let's Let's just check how we're doing on time.\u003C/p>\u003Cp>We got thirty minutes left. Let's keep cruising. Right? Alright. So we got team leads.\u003C/p>\u003Cp>Seems like enough. Let's go in and add our team members. Right? Teams members. Maybe add an icon, though.\u003C/p>\u003Cp>Team. Is there a group? This is a collective. There we go. Group.\u003C/p>\u003Cp>Looks great. Amazing. Teams. Okay. So now I want to you know, I could potentially sit in multiple teams, I guess.\u003C/p>\u003Cp>You know, obviously, you would hope that that you would have a, like, a one to one relationship within the team, but, yeah, I again, hey. Like, data modeling issue, do could a team user belong to multiple teams? I I'm gonna say yes in this case just because I don't know. You know, I maybe, like, you wanna use this as, like, a project team as well. So that's that's how we'll play this.\u003C/p>\u003Cp>Alright. Teams, we're gonna call this team members. Okay. I'm just gonna create a junction collection manually here. Like, Directus will create this for you if you want.\u003C/p>\u003Cp>I just get in the habit of wanting total total control over everything. I'm just gonna hide this guy. Alright. So now we're gonna link members to our team. And for that, we're gonna reach for our many to many relationships inside Directus.\u003C/p>\u003Cp>We're gonna call this members. The related collection is gonna be users, Directus users. Great. And I'm gonna go to the relationship tab. So on almost every episode, you'll see me jump into the advanced setup.\u003C/p>\u003Cp>Once you get into the basics with Directus, highly recommend it, because it gives me total control over the fields that are created in the junction collection. Right? Instead of using team ID, I wanna use team, and I'm just gonna use user here because I like to be OCD about certain things. Right? So now we're gonna add this teams field to direct us users so I can, you know, potentially use that in our permissions.\u003C/p>\u003Cp>And on all of these relational triggers, I'm gonna set these to cascade, right, because there's no sense in keeping this relationship in there. Alright. We could set up our interfaces, display related values. Great. There we go.\u003C/p>\u003Cp>We got our team members. I can go in and set, like, a first name, last name. I could even go in and add, like, an avatar here if I want to. Oh, avatar. There's like a little thumbnail helper.\u003C/p>\u003Cp>Oops. Let me just edit the raw value. You could see this is just the mustache syntax to populate these values, but inside the interface it shows very nicely. Alright. So now we got our team.\u003C/p>\u003Cp>Let's add some members to the team. I'm gonna add Bryant and Tess Tess to the marketing team. Great. There's our team members. I'm gonna give I gotta upload a photo here just to make sure I got something.\u003C/p>\u003Cp>Right? There we go. There's my ugly mug. There we go. So now I'm not seeing myself in there.\u003C/p>\u003Cp>We need to fix that as well. Here's where I can just copy these values. So I'll hit copy raw value. I'm just gonna paste that in the interface display template. So the difference between interface and display, the interface shows inside the form.\u003C/p>\u003Cp>Right? This is the interface. Here inside, like, the teams, if I were to look at the members, that's our display. So I wanna show the same thing. I could see those members.\u003C/p>\u003Cp>There's our team members. There's the team lead. Great. You know, I can prevent editing on all of this. Amazing.\u003C/p>\u003Cp>Solid. Right? Alright. So now, you know, we've got team members. Yeah.\u003C/p>\u003Cp>Let's add, like, our reporting structure as well with would that be within a team? This is where we get into, like, splitting errors. Right? In this case, I'm gonna add this to the user level. I guess you could add it at the the team member level, but, you know, let's not do that.\u003C/p>\u003Cp>So, anyway, I'm gonna go into our system collections here. We're going to create some new fields here. So you can already see I've got two fields. The system fields are locked down. I can't change those, but, I can add new fields to this collection, to this table in the database.\u003C/p>\u003Cp>So we're gonna add a, like, a recursive relationship here. So for that, let's just use the one to many, and this is gonna be our direct reports. Right? This is gonna be direct as users, and the foreign key would be reports two. Again, you know, naming structure here is hard.\u003C/p>\u003Cp>It would be great to have help on that side. But, alas, this is not live. I am just talking to myself. Alright. So there's our display template.\u003C/p>\u003Cp>We're gonna show a link to the item. If I want to, I can just, like, show this relationship. This is gonna be created. Reports to, direct reports. Pretty pretty straightforward.\u003C/p>\u003Cp>Right? Direct report. So you could see that it's created, like, two two or two fields here. And if I look at, like, my and, like, here's the teams leading. Here's the teams I'm a part of.\u003C/p>\u003Cp>And let's say, mister I this is gonna get confusing as I'll get a test direct report. Test manager. Okay. Alright. So I got two users here.\u003C/p>\u003Cp>I have I've got a test direct report. I don't see the other field, right, who I report to, so we can go in and edit that as well. I think by default, we hide that. Maybe I just wanna show that here. So we're gonna show reports to go in, adjust this.\u003C/p>\u003Cp>So those are my direct reports. Here is reporting to the test manager. And now if I go into, let's say, the test manager, let's take a look at that, there's the direct reports. I can also make this like a tree view as well. So if I go into users, on these recursive relationships, we can go in and set this up to use our tree view so we have this recursive relationship.\u003C/p>\u003Cp>I'm just gonna copy this, we've got tree view, paste that there. Now if I go to mister test manager, I can see my direct reports, but I can also see I'm the test manager in this scenario. I'm not Brian Gillespie. I can see the direct reports of that person and, like, all the way down the chain potentially. Right?\u003C/p>\u003Cp>So if I have mister CEO, the test manager rolls up to mister CEO, I can drill all the way down the tree, the tree, as they say. Alright. So now that we've got those relationships set up, you know, it would be nice to show, like, an org chart or something there. Let's focus on on bigger challenges at this point. Like, how can we send messages to all these folks?\u003C/p>\u003Cp>Right? We wanna provide some helpful leaks, send messages. You know, maybe we want to have a specific page. You know, as far as, like, a portal, you probably got, hey, like, pages, you know, like, struggle with what to name. Like, here's just, like, helpful content that we want to create.\u003C/p>\u003Cp>Right? So we're just gonna add these status, is this published or not? Great. Let's give this a title. And because this is internal, we we don't really need anything like a slug.\u003C/p>\u003Cp>You know, I could potentially add it if I wanted to, but, this is the page title. Here's the markdown content. Right. Great. This is our content, page contents.\u003C/p>\u003Cp>And then trying to think of the best structure here. Like, will we set up, like, different categories? I mean, this is kinda getting into, like, knowledge base level. You know, I basically, just trying to set up a structure here. Let's say marketing team should have access to that post.\u003C/p>\u003Cp>Let's say let's create another one. Marketing should not have access. Right. Alright. And let's kind of get into let's step into this as well.\u003C/p>\u003Cp>Right? Tested example, password. They've got our team member role. Cool, test at example, password. Alright, so this is what our other user is gonna see, And right now, the team member role, as we specified, has admin access, which let's go ahead and lock that down.\u003C/p>\u003Cp>Right? So let's create a new policy, team member I'm just gonna call this app access. So we're gonna give app access to the team member, and that should hide stuff like settings that we don't want them to configure. How we doing on time? Nineteen minutes remaining.\u003C/p>\u003Cp>Cruising. Alright. Team member app. Let's go back to our team member role. We're gonna remove this administrator policy.\u003C/p>\u003Cp>Alright. We don't wanna give total access. We want to give team member app access. Great. And I think I just locked myself out of the system.\u003C/p>\u003Cp>Fun for everyone. Right? But if I refresh over here where I'm logged in as our test direct report, now I don't see any of our content. I don't see any of the settings. You know, I can access the dashboard, see the file library, etcetera.\u003C/p>\u003Cp>I'm gonna dive back into table plus here. We could see there's our team members. I'm just gonna go in and quickly fix this goof up that I just made where I removed myself as an administrator. There we go. Alright.\u003C/p>\u003Cp>So now if I reload that, I've got access back again. And again, you can see this is all just underlying SQL. Nothing nothing fancy here. Right? Alright.\u003C/p>\u003Cp>Marketing should have access to this. Marketing should not have access. You know, I would probably go one layer deep if I had, like, a a lot of these and say, okay. You know, I'll group these by categories. And, you know, like, that way you could restrict access, but let's just do it this way.\u003C/p>\u003Cp>We're going to create another mini to mini teams with access to this content. Yeah. What else could we do? A Couple other ways we could do this. Right?\u003C/p>\u003Cp>Yeah. Let's just do the many to many to keep it relational. Teams. Team access. Teams access.\u003C/p>\u003Cp>Teams. Great. Show a link to the item. Alright. And we could see page, team access.\u003C/p>\u003Cp>That's gonna be the page. That's gonna be the team. This is pages that they have access to, and I'll show you why this is gonna matter in a moment. Alright. So title content, marketing should have access.\u003C/p>\u003Cp>We can say marketing. Great. Pages marketing should not have access to. That's gonna be product and sales should have access to those. Okay.\u003C/p>\u003Cp>Now by default, no permissions are set for collections. Right? So if I go into team member app policy, I've just got the system collections that they need. This is why I love the access policies because I can also keep those separate. And I could say can access pages or read pages or something like that.\u003C/p>\u003Cp>Right? I don't have to do any of these other options. I can just go in and say pages, pages team access. So by default, let's set these up first. Maybe we wanna be able to read the teams.\u003C/p>\u003Cp>That's fine enough, we can see all the team members. But when it comes to page access, right, what we want is to restrict that based on teamaccess.ID. No. Teamaccess.team equals or is one of current user. So this is a dynamic variable inside Directus, and, basically, we can go through the tree here.\u003C/p>\u003Cp>So this will get us our current user, like, who's logged in. We could go through teams and then Teams.team. I believe Teamsteam. Is that gonna be our junction table collection? Should be.\u003C/p>\u003Cp>Right? Here's our team members. There's the team. Alright. Let's just test this out and see.\u003C/p>\u003Cp>I don't see any nothing. Right? Well, we created that access policy, but we didn't apply it to our team member role. So here's our policies. Right?\u003C/p>\u003Cp>Team member app. They can just log in to the app. And now we're gonna give them access to pages. Hit save and say. And if I did my homework or if I did everything correctly, this should be all set up.\u003C/p>\u003Cp>Right? So there's marketing. I could see these. I can't edit any of them. And as far as the pages go, marketing should have access to this, but they should not have access to that.\u003C/p>\u003Cp>Bada bing, badda boom. That's all done. Right? Amazing. It just works out of the box.\u003C/p>\u003Cp>And the beautiful part about this is not only is this through the UI, right, where I'm logged in, this is respected throughout the API as well. So if I, like, do this API call, oh, I don't need to add API in front of it. Used to working with Nuxt. Items dot pages, there's that specific page. But if I just fetch items pages, again, I'm only seeing the one page.\u003C/p>\u003Cp>Whereas if I do it over here in this browser, you could see I've got two pages, because I am logged in as an admin, and admins have full permissions. Great. Alright. I'm gonna check this off as the helpful links. You know, couple ways we could send messages to our users.\u003C/p>\u003Cp>We've obviously got, like, an email. I don't think we have a phone field in here. We could add a field to track phone numbers for our team members. We could, you know, message them via Twilio without, say, exposing, phone numbers or private information to other members of the team. Right?\u003C/p>\u003Cp>Do we have enough time for something like Twilio? Let's just let's cover the email use case. Right? I want to craft a message to, let's say, team announcements. Right?\u003C/p>\u003Cp>And I want to draft this message. I wanna send it out to all of our team members. Too many n's in there. Thank you. Spell check.\u003C/p>\u003Cp>Date created. Cool. We'll generate a UID. Got it. And we got the message body, and we want to do what?\u003C/p>\u003Cp>Message body is just gonna be WYSIWYG. That's our content. We could just call it content, or, yeah, you might even do text. Fine. Keep it simple.\u003C/p>\u003Cp>Alright. Alright. And then we're gonna do, like, a mini to mini for our teams that we wanna message. Right? Maybe I wanna message individual teams.\u003C/p>\u003Cp>So we'll do the related collection. And, again, like, behind the scenes right now, Directus Us is crew will create those junction tables for me. Again, I just won't have the control unless I go into this team announcement teams, team announcement teams, team announcement. Let's just call it announcement. We're gonna call that team.\u003C/p>\u003Cp>Alright. And this is like a, an insane way to do this, but I I could really just create, like, a a flow. But maybe I wanted to, like, schedule this as well. Right? Okay.\u003C/p>\u003Cp>So here's our team announcements. I want to add a announcement for the marketing team. And, hey. This is an announcement from me. Alright.\u003C/p>\u003Cp>So, you know, maybe I'm, like, drafting this. You know, maybe I don't wanna send this right away. Maybe we track, like, a another time stamp for date sent, date scheduled, that sort of thing. But we've got this announcement. Right?\u003C/p>\u003Cp>I there's a couple of ways that we could do this as well, but let's create a flow for this. Right? So I'm gonna go in, let's say, send announcement, and we're just gonna manually trigger this. So once we have the announcement, we wanna send it out. We're gonna require a confirmation.\u003C/p>\u003Cp>Send announcement. K. And I could actually use, like, this flow to, you could build forms into this, right, which is another nice thing. I could say, hey, form field. This is a field.\u003C/p>\u003Cp>So when you're doing these manual flows inside Directus, it's always nice to have this ability to just stick a form field in there. And from here, hit send announcement. I can add something to this field, run this flow, and that will give me some data. Right? So here I can see the body.\u003C/p>\u003Cp>Here's the keys that we're receiving. So this is the actual announcement. And we could do something like this, where we're going to, what are we going to do? We're going to read the data. So we're going to get the announcement.\u003C/p>\u003Cp>Announcements. That's going to be your Team Announcements. The IDs here are gonna be trigger dot body dot keys. I'm just gonna hit enter to save that. That.\u003C/p>\u003Cp>I don't need to run a filter on that. And then, also, I'm gonna get the team members. Right? So if I look at my payload one more time oh, no. We'll get the we'll get the team members from the actual announcements.\u003C/p>\u003Cp>Right? So here, I'm gonna do something like this where I have fields. Fields allows you to fetch what you need in a GraphQL like manner even though we're using REST. So I don't necessarily have to reach for the complexity of GraphQL if I don't need it. So I'm gonna get all the root level fields with an asterisk, then I am going to get teams, and I want teams dot member dot star.\u003C/p>\u003Cp>I'm just gonna test that out and see. I think it's depending on how we set up that. We've got teams, team members. Oh, it's gonna be user, not member. Alright.\u003C/p>\u003Cp>Let's test this out. Teams dot user teams dot user dot star. We'll save that. Alright. Now, what do we got?\u003C/p>\u003Cp>Six minutes left. Let's crush this here. We're going to send the announcement. Send that out. Hit flows.\u003C/p>\u003Cp>We get send announcements. There's our payload error, UUID. Some kind of issue here. Oh, we're passing an array. Let's not pass an array.\u003C/p>\u003Cp>Let's just pass the array of arrays. Let's just pass the single array. We're gonna hit send announcement here. Test that out. Get our flow.\u003C/p>\u003Cp>Send announcement. This is an announcement. Okay. So we got teams. I'm not getting the actual team members that we need here because we need, like, teams.\u003C/p>\u003Cp>Teams. I should be getting the team data. Right? And I should be getting that. Teams.\u003C/p>\u003Cp>Why am I not getting that data that I want? Let's try it again. Send flows. Send announcement. We're gonna take a look at this.\u003C/p>\u003Cp>Okay. Okay. Team.team.marketing. And then can I get through users from there? Teams.starTeam.star.\u003C/p>\u003Cp>Team Members Star. Love waiting through a good data model here. Let's see what we get now. Test, flows, send announcement. Okay.\u003C/p>\u003Cp>So now we're getting closer. Right? Here's our team. There's our members. We got the users.\u003C/p>\u003Cp>Those are what we're trying to get to. Teams.user.star. This is gonna give me the emails of the team members. Once I get that, send announcement. Flows.\u003C/p>\u003Cp>Is this gonna give us the data of what we want? Yes. There we go. Alright. And, really, all I need here is the email.\u003C/p>\u003Cp>This is where I'm going to turn to AI for, like, three minutes worth here. Let's just open cursor. Write a function, a JS function that returns the array of emails for this sample data. There we go. First, we need to okay.\u003C/p>\u003Cp>Spit something out, friend. Data dot teams, that flat map. Okay. So there's a simple this is writing TypeScript, which we're not gonna be able to use in flows, but you do have the ability to run just JavaScript. We'll call this transform.\u003C/p>\u003Cp>Let's just pop in that function. We'll remove the types. Data data dot teams. Alright. So we're gonna do this.\u003C/p>\u003Cp>We're going to get the, what did we call that last one? This is just announcement. We're gonna get the announcement data. So here's what we're gonna do. We'll do, this doesn't need to be async.\u003C/p>\u003Cp>We're gonna say const, actually, we're just gonna return get emails, and the data is gonna be dot data dot announcement. So as you go through a flow, like, each step will append the result under the key. So this data object that we pass, I can access the announcement data for that step through this. And then the final step here is gonna be sending an email, which I don't think I've actually I can't remember if I got emails hooked up or not. We're gonna get this transform.\u003C/p>\u003Cp>We're going to reference that. So I'm gonna send the email. We're going to say transform, transform. And I'm gonna make sure that that is that should be passing an array. So we're just gonna use the raw value.\u003C/p>\u003Cp>I don't have a this is an important announcement. And then for the WYSIWYG content, we are going to do announcements dot text. Alright. We got forty five seconds. We're only gonna get one go at this probably.\u003C/p>\u003Cp>Let's see if this is actually gonna work. Run this flow. Send announcement. Is it actually going to run the flow to brian@directus.io? Did I configure this?\u003C/p>\u003Cp>Yes or no? Come on, baby. Come on. It shows my email is disconnected. Gmail.\u003C/p>\u003Cp>Come on. Come on, Gmail. Fifteen seconds. Are we gonna get an announcement? Are we gonna get an announcement?\u003C/p>\u003Cp>Are we gonna get an announcement? No. I don't think I've got email connected inside this actual Docker Compose file, to be honest with you. Too bad. Too bad.\u003C/p>\u003Cp>We didn't get to the last one. That sucks. Sending messages to the entire team, it should go through. And if I were on a Directus cloud instance, it probably would go through, but I don't have the actual email drivers conf like, the config set up for that. So, like, no emails are going through there.\u003C/p>\u003Cp>You know, obviously, I would wanna take the time as a next step to just go ahead and do that. So, if I go to, like, the direct us documentation and I look for email, you know, I would want to set up, like, my either my SMTP settings or Mailgun or SES, to actually send those emails out. But I will I I don't know if we're gonna call this a win or not because it did run this flow. It did give us the emails. We're not getting any errors there.\u003C/p>\u003Cp>We're just, yeah. I don't know. Let's let's just do the explosion anyway. That's my favorite animation of this whole series. So that is building a company Internet internal portal.\u003C/p>\u003Cp>Hope this was a helpful look at just how powerful Directus is. I will catch you on the next episode of 100 apps, one hundred hours.\u003C/p>","Welcome back to yet another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. This is the show where we try to build or rebuild some of your favorite apps in one hour or less or get publicly shamed trying. Alright. So if you're new here, there are only two rules. Sixty minutes to plan and build, which always results in fun and chaos. And the second rule, use whatever you have at your disposal, AI tools, past projects, random packages off of the Internet, whatever. It's fair game. Alright. Let's dive in. Today, we are going to be building a company intranet portal thing. Listen. This was honestly a suggestion from, some of the AEs on our team. Love those guys. They're amazing. The spec for this, very fuzzy. Right? I I don't even know what is in a company Internet portal sort of thing these days. I do know excuse me. I do know that I don't think I ever enjoyed using one. So we're gonna try to create something in an hour or less that is helpful and actually decent to use. Let's dive in and hit the clock. Alright. Sixty minutes. Usually, let's kick this off with functionality. Right? What type of functionality do we want out of an intranet portal? And we're gonna do single sign on for sure. Right? If we're using something internal, we want single sign on maybe through Google or Microsoft or something like that. We're gonna set that up. Yeah. We wanna be able to track team members and show org structure, you know, maybe provide helpful links. I feel like maybe that could be like a Google Doc, but, you know, we're we're doing the fancy version of this. And then what else do we're gonna have? You know, maybe I wanna be able to send messages to entire team. Great. Okay. So that is what we're looking at as far as functionality. Let's kind of discuss our data model. Right? We're going to need honestly, there's new shapes there. Figma, come on now. Okay. I don't I don't need a tour on the new shapes. Thank you, though. Alright. Getting derailed by the tools already. We're, like, two minutes into this. Alright. So, obviously, we are using Directus on the back end here. That's gonna give me a lot out of the box, like, user permissions, like users. Right? So we're gonna have users of the application. Those are also gonna double as team members. So one shot, one kill here. You know, users comes with emails, passwords, first name, last name, job title. We're gonna probably want to add, like, reporting structure, direct reports, reports to, something like that so I can establish those relationships. We probably want, like, a team. So we got different teams within the organization, so that'll be a separate one. Why is that text so small? Oh, too big. Too big. Okay. Alright. So we got teams. We got users. What else do we have? You know, maybe, like, pages or resources. I guess really fuzzy. Right? And this is the part of the show where I draw these amazing arrows that that FigJam allows for. So k. Looks great. You know? And then maybe there's a relationship here where some team members have access to certain pages and some don't, that sort of thing. Let's see what we can get done. Right? Alright. So what have I got set up already? Basically, absolutely nothing. I've got a example project or a a Docker Directus instance running here. I've got, you know, just kind of like a standard Docker Compose setup. Don't have much config in this. No collections setup whatsoever. Alright. So let's tackle first thing on the list. Right? I want to set up single sign on. So if I log out of this instance, I see email and password, log in. I wanna do single sign on for, like, Google, for instance. Alright. So how are we going to configure that? Well, inside our documentation, we've got a list of, like, the configuration that we could set up. So we're gonna have, like, an auth providers. So that's a comma comma separated list of auth providers. The driver that we're gonna use and then a provider mode, we'll probably use session. And then for Google, I think the recommendation from our team is to use OpenID. So we've got, like, an OpenID setup here. Now do we have, like, a Google SSO single sign on? This this is the single sign on dock. Do we have a, like, standard config? Somewhere where we could see, like, a standard config for this. Yeah. There we go. Alright. So we could set up multiple auth providers, but this looks like the the pretty standard config for, Google. So I'm just gonna copy this wholesale. Right? I'm gonna go inside my Docker Compose, you know, or I could stick this in my ENV file if I wanted to. I'm not gonna open that because I've got some other secrets in there. As far as the IDE I'm using, this is Cursor. I've been using that this entire season. I found it pretty helpful and especially for quick POCs like this. Alright. So now we got some config in here. We're gonna have to go and create a client inside Google as well. So let's go to Google Google SSO single sign on, console. Cloud. Google, I think, is the let's see what we got here. Log in as Directus. Alright. Let's get started configuring. I've got my first project, so you're probably gonna have to create a project if you don't have one. Let's get started. We're gonna call this 100 apps, one hundred hours. The user support email, that's gonna be me, Brian at Directus. This will be an internal audience. So, my knowledge of Google auth is very minimal, But, obviously, internal is gonna be users only within our organization, which for this works well. Right? When I'm at directus dot I o. Great. I agree. We're going to spin this up. Okay. Nice. Nice. Okay. We're gonna create an OAuth client. This is a web application. Let's call this Directus. Alright. So what do we need to do as far as, like, our JavaScript origins and our authorized redirect URLs? So I think we're gonna have to do something like this where we have local host 8055 as far as our origin. Let's see if we can search our doc single sign on. Here's a entire guide on this. So you could just search that up. Right? Internal authorized domains. Alright. So we're gonna authorize JavaScript origins. That's gonna be it's not required, but that's gonna be our directus server address. And then the authorized callback is gonna be slash auth slash login slash Google slash callback. And that's because of the auth provider key that we're providing to the auth provider is Google. Alright? So that's how we reference this in our actual config. Alright. So, let's add that. That'll be HTTP local host 8 0 5 5. Great. We're gonna hit create. I certainly hope it doesn't take as long as what it's saying. Alright. So if I expand this, over here we're gonna get a client ID. We're gonna copy this client ID. That's gonna go here. Great. Then we got a secret. Don't steal my secrets. We're gonna paste that here. And what else do we need? Let's add an auth Google mode. Google mode. We're gonna make that session. The identifier key is email. I'm also gonna do allow public registration. Google allow public registration. So we want anybody who comes through our organization to be able to register for this. And one of the other things that we could do, I'm just gonna copy and paste. We probably wanna add, like, a default role setup as well. We'll get to that in a moment. Default role ID. Let's just actually dive into that right now. Alright. So great. We are going to sign in. If you remember the admin password, that is okay. So now we got this. Let's go in and create we've got two roles by default. These are out of the box for Directus. We've got public. This role controls, you know, like, what is available without authentication. And there's also a public access policy. So the access policies are, you know, just groups of different permissions, and then you can add many different access to a role. I love the granularity here, and it's super easy to maintain, like, a complex permission system between the both of these. What we do wanna do here is basically let's create a new role. We're gonna call this a team member role. Great. Okay. Alright. So we'll just save that. I'm gonna grab the primary key for this. That's what I'm gonna stick here for the role ID. That's the role that we want folks to have. As far as the policies that we're gonna give them, for now, let's just give them full admin access. They could change anything. We're gonna scope that down eventually, though. Alright. So the next thing I'm gonna do to test this out, right, I want to I I just want to stop my container. I'm gonna spin that container back up, and I'm gonna open this in. Let's do an incognito window with the admin login. Admin and example. Password. Great. K. And now if I log out of this one because I'm I'm already signed in to Google for everything here. Alright. If I hit bryant at directus, create a strong password. Why do I have to create a strong password? Come on, dawg. Why is it asking me to do this? Use 20 characters or more. Come on, guys. What is it? What are we doing? I'm gonna do this off screen here. I don't I don't know why this is happening here. Google. Let's create a new password. 20 characters or more. Directus dot I o. Okay. Oh my gosh. Let's see if this is actually gonna work. Sign in to 100 apps, one hundred hours. The request cannot be processed because it is malformed, which is not good. Let's try it again. What are we doing wrong here? So we go back to our Google where's our Google application? Yeah. Sign in. Yes. It's me. Love it when all of this nice security tech gets in our way. What are we missing? Right? HTTP local host 8055 slash login slash google/callback. Okay. See, this is nice because I've tried to log in to a different account, but I should be able to log in to there we go. So now I am in. Right? And we can see that Bryant Gillespie is created. This is this is really nice. Right? We could see the auth provider here is Google. There's the external modifier or identifier, not modifier. Now we've got single sign on. Right? And one of the other things that I could potentially do I think there's config for this if we take a look. Go into auth and SSO. I could disable the default provider as well if I wanted to so that you could only log in with single sign on. So if we do that, let's see what we've got. Auth, disable default, true. And then I'm gonna restart my container. Let's just log back out. And now there's only Google to log in. There is no email and password auth options. This will disable it at the API level as well. Super nice thing to do. Right? If if all you want to allow is single sign on. So, with that done, right, let's let's go in and brand this thing because we obviously need a nice brand. Huge fan of office space. If you're younger and you're watching this, you know, this is an older movie. It pains me to say that. But, let's use Inatec as the company here. So I'm gonna go into our appearance settings, the project logo. So I could just copy the image address, take full advantage of this a lot of times. So I'll just import the file from a URL. If I hit save, we could see that Inatec logo up there. I'm gonna use the color picker. Let's pick up, like, a Inateq blue, maybe, I guess we could call that. Right? We got, like, this pale blue color. I'm gonna set up the Directus default theme, and in the namesake of the show, I'm gonna steal from our simple CMS template. I've got, like, a custom theme that I really like, super happy with. So I'm just gonna steal that paste raw value. There we go. We might wanna swap this out. And one of the nice things about the theming in Directus is, you know, I've got all these nice keys for theming, but I also have the ability just to add custom CSS and do whatever I want. So in this case, we'll do that as well. And what else are we gonna do? I'm gonna fix this just because it's gonna aggravate me. Alright. So this is where that CSS comes in. I've got the module bar logo. I'm gonna do something like this where we have dot module bar logo, background. Let's just do white. And we're gonna stick important on that. There we go. Now we're cooking. And maybe we wanna add, like, a subtle subtle border there. Let's see. What is that gonna be? That'll be our navigation. That'll be the modules. That'll be the border color, which would be I don't know. Let let's use color mix in sRGB. We'll use the var theme. We're gonna use the primary color and maybe 20% white. F f f f. And let's set, like, a two pixel border just so we can see it. Alright. There we go. So now we got a little little border action to keep things separated. I mean, this is not like a super nice mouse over active color, but I'm not gonna get too far into it. Right? This is gonna be the Inatec company portal. And one of the things that's gonna aggravate me is just our font choice here. Like, DM Sans is a newer font. Maybe we just set this to Arial. Do I have that installed? Would it be Arial? Yeah. I think it is one way or the other. And then we can set, like, our, let's let's pick something, like, super corporate here. Yeah, Roboto feels pretty corporate to me at this point. We'll do Roboto. So if I just put this in quotation marks, this will go ahead and pull those font files from Google for you. So any Google font, good, bad, ugly, whatever you wanna do, you can include those when you're customizing these themes. Alright. So now we got Inatek. We've got Roboto. This feels very robotic. This is a sad company worked for fictionally. So now if I refresh our login screen, we've got this kind of action going on. Looks okay. Maybe we wanna spice this up a bit. Right? So we're just gonna put Bill Lemberg on the login page. So we'll go to appearance. The public background here, we're gonna import Lemberg. Yeah. We need those TPS reports, and, you know, I will just leave the Favicon blank. There it is. There we go. Right? So this is the it's beginning to shape up as a perfect company portal. Alright. And one thing I'm gonna do, I'm gonna turn this off, or we're not gonna be able to test, like, our permissions. So disable default. Let me just spin this back up so we can get, like, an email and password. Log in one more time. Sign out. Okay. So now I've got my email and password back. Let's dive into the next item on our list, single not single sign on. We just completed that one. We're gonna dive into tracking team members and showing, like, an org structure, providing helpful links, and maybe try to send messages to the entire team. Alright. So, we've already got users through Directus. Right? We've got our user library here. We could see that Brian logged in, etcetera, blah blah blah. Looking great. Let's add this team functionality. Right? And we probably want the ability to create new teams and adjust teams and etcetera. So let's just create a collection for this. I could also just adjust the users collection, which we'll do in a few. But for this case, I'm gonna create teams. So each team, we need a primary key field. I could use an auto incremented integer. We can use a manually entered string. You know, I mean, like, in this case, maybe we use a manually entered string because I'm not expecting to have a a ton of teams, and, you know, the way we reference those is probably not gonna change much. Alright. Do we need any of these default fields? I'm not really sure. I don't think so. So we'll just skip that. So we got a team. We're gonna give a team name. Pretty simple. We'll make sure that is required. Great. The ID, we can make sure this is like URL safe, so we'll just enable Slugify through the interface settings. And we've got a team, we've got a name. Now what we're gonna do is set up a relationship between we probably got, like, a a parent and child relationship between teams as well. Right? One team could roll up to another team, could roll up to etcetera. You know, we could classify things like a team lead, and then we're gonna have, like, an organizational structure where we have direct reports for each user as well. So let's set up that first one. Right? Each team needs a leader. We'll just call this team lead. So here, I'm using the direct us relationships. I'm gonna set up a many to one because I only want one leader within the team. Could certainly set up multiple leaders if I wanted to, but for the related collection here, I'm gonna use direct us users. And as soon as I hit on the right one, you'll see that kind of turn blue and lock in. I can also just go over here and hit the system collections and do it that way as well. We're gonna show a link, and I'm gonna open up Advanced Field Creation Mode just so we could see this. What I'm also gonna do here, I can create the corresponding relationship at the same time. And this is where data modeling becomes very important. Right? Because, do we necessarily want this relationship here? Because I could be leading, you know, I guess I could lead multiple teams. So we will add this relationship here, but, you know, if you need, like, multiple team leads, obviously, you're looking at, like, many to many. We're gonna display the direct as user, show the user in a circle. We'll set that up. So there's the team lead. And now if I were to go and check our data model for users, right, I should also have a Teams option here. Right? There's our Teams. So we can specify the do I actually want that? The we're gonna have Teams as another thing, like, teams that we're a part of. Let's delete this, recreate this relationship, one to many, and teams leading. It's not t lead teams leading. Yeah. Leads, teams, teams leading, I guess. Naming stuff is I say this on every episode, I feel like, but naming stuff is the hardest thing in programming without a doubt. Alright. There we go. We got the display table, and we're gonna show the name. Cool. Team lead already has an associated relationship. What are we doing here? Have I bricked this whole thing? Let's see what we've got. Can I get access to the relationships here? If I reload what is going on? Have I totally bricked Directus with this relationship? What have I done? People, what have I done? Permissions, policies, fields, direct us users, team leads. Now if I refresh, did this sort it out? What have I done? Alright. Teams. Team lead. I don't understand. Teams users leading does not exist. Let's just try down and up. I goofed up our relationship somehow. I think we can get this back. We can solve this problem. Now I would be remiss not to take this opportunity to talk about the data mirroring inside Directus. So, you can install Directus onto any existing SQL database, whether that's Postgres, SQLite, MySQL. Directus, like, bootstraps these name space collections as you see here, but everything else is just standard SQL. Right? There's nothing direct as specific about our team teams collection here. And as I go through and build these out, let's try this one more time. One to many relationship or actually a many to one here. We're gonna have a team lead. We'll pick our direct to users collection, and then in the relationship settings, I'm gonna add that one, teams leading. I hate that, but that's what we're rolling with for now. We're gonna create this relationship. Right? Now, we could see that that change is being mirrored here. There's my team lead. Amazing. Right? So I could go in. We could create a new team. We could do team America. Let's just call it bride's team or let's do marketing, I guess. Marketing. Great. Mister admin user can be our team lead, and then I can go in and add additional teams. Right? Product. Got the product team. Sales. Great. You know, we could get really fancy and add icons for all these teams. Let's Let's just check how we're doing on time. We got thirty minutes left. Let's keep cruising. Right? Alright. So we got team leads. Seems like enough. Let's go in and add our team members. Right? Teams members. Maybe add an icon, though. Team. Is there a group? This is a collective. There we go. Group. Looks great. Amazing. Teams. Okay. So now I want to you know, I could potentially sit in multiple teams, I guess. You know, obviously, you would hope that that you would have a, like, a one to one relationship within the team, but, yeah, I again, hey. Like, data modeling issue, do could a team user belong to multiple teams? I I'm gonna say yes in this case just because I don't know. You know, I maybe, like, you wanna use this as, like, a project team as well. So that's that's how we'll play this. Alright. Teams, we're gonna call this team members. Okay. I'm just gonna create a junction collection manually here. Like, Directus will create this for you if you want. I just get in the habit of wanting total total control over everything. I'm just gonna hide this guy. Alright. So now we're gonna link members to our team. And for that, we're gonna reach for our many to many relationships inside Directus. We're gonna call this members. The related collection is gonna be users, Directus users. Great. And I'm gonna go to the relationship tab. So on almost every episode, you'll see me jump into the advanced setup. Once you get into the basics with Directus, highly recommend it, because it gives me total control over the fields that are created in the junction collection. Right? Instead of using team ID, I wanna use team, and I'm just gonna use user here because I like to be OCD about certain things. Right? So now we're gonna add this teams field to direct us users so I can, you know, potentially use that in our permissions. And on all of these relational triggers, I'm gonna set these to cascade, right, because there's no sense in keeping this relationship in there. Alright. We could set up our interfaces, display related values. Great. There we go. We got our team members. I can go in and set, like, a first name, last name. I could even go in and add, like, an avatar here if I want to. Oh, avatar. There's like a little thumbnail helper. Oops. Let me just edit the raw value. You could see this is just the mustache syntax to populate these values, but inside the interface it shows very nicely. Alright. So now we got our team. Let's add some members to the team. I'm gonna add Bryant and Tess Tess to the marketing team. Great. There's our team members. I'm gonna give I gotta upload a photo here just to make sure I got something. Right? There we go. There's my ugly mug. There we go. So now I'm not seeing myself in there. We need to fix that as well. Here's where I can just copy these values. So I'll hit copy raw value. I'm just gonna paste that in the interface display template. So the difference between interface and display, the interface shows inside the form. Right? This is the interface. Here inside, like, the teams, if I were to look at the members, that's our display. So I wanna show the same thing. I could see those members. There's our team members. There's the team lead. Great. You know, I can prevent editing on all of this. Amazing. Solid. Right? Alright. So now, you know, we've got team members. Yeah. Let's add, like, our reporting structure as well with would that be within a team? This is where we get into, like, splitting errors. Right? In this case, I'm gonna add this to the user level. I guess you could add it at the the team member level, but, you know, let's not do that. So, anyway, I'm gonna go into our system collections here. We're going to create some new fields here. So you can already see I've got two fields. The system fields are locked down. I can't change those, but, I can add new fields to this collection, to this table in the database. So we're gonna add a, like, a recursive relationship here. So for that, let's just use the one to many, and this is gonna be our direct reports. Right? This is gonna be direct as users, and the foreign key would be reports two. Again, you know, naming structure here is hard. It would be great to have help on that side. But, alas, this is not live. I am just talking to myself. Alright. So there's our display template. We're gonna show a link to the item. If I want to, I can just, like, show this relationship. This is gonna be created. Reports to, direct reports. Pretty pretty straightforward. Right? Direct report. So you could see that it's created, like, two two or two fields here. And if I look at, like, my and, like, here's the teams leading. Here's the teams I'm a part of. And let's say, mister I this is gonna get confusing as I'll get a test direct report. Test manager. Okay. Alright. So I got two users here. I have I've got a test direct report. I don't see the other field, right, who I report to, so we can go in and edit that as well. I think by default, we hide that. Maybe I just wanna show that here. So we're gonna show reports to go in, adjust this. So those are my direct reports. Here is reporting to the test manager. And now if I go into, let's say, the test manager, let's take a look at that, there's the direct reports. I can also make this like a tree view as well. So if I go into users, on these recursive relationships, we can go in and set this up to use our tree view so we have this recursive relationship. I'm just gonna copy this, we've got tree view, paste that there. Now if I go to mister test manager, I can see my direct reports, but I can also see I'm the test manager in this scenario. I'm not Brian Gillespie. I can see the direct reports of that person and, like, all the way down the chain potentially. Right? So if I have mister CEO, the test manager rolls up to mister CEO, I can drill all the way down the tree, the tree, as they say. Alright. So now that we've got those relationships set up, you know, it would be nice to show, like, an org chart or something there. Let's focus on on bigger challenges at this point. Like, how can we send messages to all these folks? Right? We wanna provide some helpful leaks, send messages. You know, maybe we want to have a specific page. You know, as far as, like, a portal, you probably got, hey, like, pages, you know, like, struggle with what to name. Like, here's just, like, helpful content that we want to create. Right? So we're just gonna add these status, is this published or not? Great. Let's give this a title. And because this is internal, we we don't really need anything like a slug. You know, I could potentially add it if I wanted to, but, this is the page title. Here's the markdown content. Right. Great. This is our content, page contents. And then trying to think of the best structure here. Like, will we set up, like, different categories? I mean, this is kinda getting into, like, knowledge base level. You know, I basically, just trying to set up a structure here. Let's say marketing team should have access to that post. Let's say let's create another one. Marketing should not have access. Right. Alright. And let's kind of get into let's step into this as well. Right? Tested example, password. They've got our team member role. Cool, test at example, password. Alright, so this is what our other user is gonna see, And right now, the team member role, as we specified, has admin access, which let's go ahead and lock that down. Right? So let's create a new policy, team member I'm just gonna call this app access. So we're gonna give app access to the team member, and that should hide stuff like settings that we don't want them to configure. How we doing on time? Nineteen minutes remaining. Cruising. Alright. Team member app. Let's go back to our team member role. We're gonna remove this administrator policy. Alright. We don't wanna give total access. We want to give team member app access. Great. And I think I just locked myself out of the system. Fun for everyone. Right? But if I refresh over here where I'm logged in as our test direct report, now I don't see any of our content. I don't see any of the settings. You know, I can access the dashboard, see the file library, etcetera. I'm gonna dive back into table plus here. We could see there's our team members. I'm just gonna go in and quickly fix this goof up that I just made where I removed myself as an administrator. There we go. Alright. So now if I reload that, I've got access back again. And again, you can see this is all just underlying SQL. Nothing nothing fancy here. Right? Alright. Marketing should have access to this. Marketing should not have access. You know, I would probably go one layer deep if I had, like, a a lot of these and say, okay. You know, I'll group these by categories. And, you know, like, that way you could restrict access, but let's just do it this way. We're going to create another mini to mini teams with access to this content. Yeah. What else could we do? A Couple other ways we could do this. Right? Yeah. Let's just do the many to many to keep it relational. Teams. Team access. Teams access. Teams. Great. Show a link to the item. Alright. And we could see page, team access. That's gonna be the page. That's gonna be the team. This is pages that they have access to, and I'll show you why this is gonna matter in a moment. Alright. So title content, marketing should have access. We can say marketing. Great. Pages marketing should not have access to. That's gonna be product and sales should have access to those. Okay. Now by default, no permissions are set for collections. Right? So if I go into team member app policy, I've just got the system collections that they need. This is why I love the access policies because I can also keep those separate. And I could say can access pages or read pages or something like that. Right? I don't have to do any of these other options. I can just go in and say pages, pages team access. So by default, let's set these up first. Maybe we wanna be able to read the teams. That's fine enough, we can see all the team members. But when it comes to page access, right, what we want is to restrict that based on teamaccess.ID. No. Teamaccess.team equals or is one of current user. So this is a dynamic variable inside Directus, and, basically, we can go through the tree here. So this will get us our current user, like, who's logged in. We could go through teams and then Teams.team. I believe Teamsteam. Is that gonna be our junction table collection? Should be. Right? Here's our team members. There's the team. Alright. Let's just test this out and see. I don't see any nothing. Right? Well, we created that access policy, but we didn't apply it to our team member role. So here's our policies. Right? Team member app. They can just log in to the app. And now we're gonna give them access to pages. Hit save and say. And if I did my homework or if I did everything correctly, this should be all set up. Right? So there's marketing. I could see these. I can't edit any of them. And as far as the pages go, marketing should have access to this, but they should not have access to that. Bada bing, badda boom. That's all done. Right? Amazing. It just works out of the box. And the beautiful part about this is not only is this through the UI, right, where I'm logged in, this is respected throughout the API as well. So if I, like, do this API call, oh, I don't need to add API in front of it. Used to working with Nuxt. Items dot pages, there's that specific page. But if I just fetch items pages, again, I'm only seeing the one page. Whereas if I do it over here in this browser, you could see I've got two pages, because I am logged in as an admin, and admins have full permissions. Great. Alright. I'm gonna check this off as the helpful links. You know, couple ways we could send messages to our users. We've obviously got, like, an email. I don't think we have a phone field in here. We could add a field to track phone numbers for our team members. We could, you know, message them via Twilio without, say, exposing, phone numbers or private information to other members of the team. Right? Do we have enough time for something like Twilio? Let's just let's cover the email use case. Right? I want to craft a message to, let's say, team announcements. Right? And I want to draft this message. I wanna send it out to all of our team members. Too many n's in there. Thank you. Spell check. Date created. Cool. We'll generate a UID. Got it. And we got the message body, and we want to do what? Message body is just gonna be WYSIWYG. That's our content. We could just call it content, or, yeah, you might even do text. Fine. Keep it simple. Alright. Alright. And then we're gonna do, like, a mini to mini for our teams that we wanna message. Right? Maybe I wanna message individual teams. So we'll do the related collection. And, again, like, behind the scenes right now, Directus Us is crew will create those junction tables for me. Again, I just won't have the control unless I go into this team announcement teams, team announcement teams, team announcement. Let's just call it announcement. We're gonna call that team. Alright. And this is like a, an insane way to do this, but I I could really just create, like, a a flow. But maybe I wanted to, like, schedule this as well. Right? Okay. So here's our team announcements. I want to add a announcement for the marketing team. And, hey. This is an announcement from me. Alright. So, you know, maybe I'm, like, drafting this. You know, maybe I don't wanna send this right away. Maybe we track, like, a another time stamp for date sent, date scheduled, that sort of thing. But we've got this announcement. Right? I there's a couple of ways that we could do this as well, but let's create a flow for this. Right? So I'm gonna go in, let's say, send announcement, and we're just gonna manually trigger this. So once we have the announcement, we wanna send it out. We're gonna require a confirmation. Send announcement. K. And I could actually use, like, this flow to, you could build forms into this, right, which is another nice thing. I could say, hey, form field. This is a field. So when you're doing these manual flows inside Directus, it's always nice to have this ability to just stick a form field in there. And from here, hit send announcement. I can add something to this field, run this flow, and that will give me some data. Right? So here I can see the body. Here's the keys that we're receiving. So this is the actual announcement. And we could do something like this, where we're going to, what are we going to do? We're going to read the data. So we're going to get the announcement. Announcements. That's going to be your Team Announcements. The IDs here are gonna be trigger dot body dot keys. I'm just gonna hit enter to save that. That. I don't need to run a filter on that. And then, also, I'm gonna get the team members. Right? So if I look at my payload one more time oh, no. We'll get the we'll get the team members from the actual announcements. Right? So here, I'm gonna do something like this where I have fields. Fields allows you to fetch what you need in a GraphQL like manner even though we're using REST. So I don't necessarily have to reach for the complexity of GraphQL if I don't need it. So I'm gonna get all the root level fields with an asterisk, then I am going to get teams, and I want teams dot member dot star. I'm just gonna test that out and see. I think it's depending on how we set up that. We've got teams, team members. Oh, it's gonna be user, not member. Alright. Let's test this out. Teams dot user teams dot user dot star. We'll save that. Alright. Now, what do we got? Six minutes left. Let's crush this here. We're going to send the announcement. Send that out. Hit flows. We get send announcements. There's our payload error, UUID. Some kind of issue here. Oh, we're passing an array. Let's not pass an array. Let's just pass the array of arrays. Let's just pass the single array. We're gonna hit send announcement here. Test that out. Get our flow. Send announcement. This is an announcement. Okay. So we got teams. I'm not getting the actual team members that we need here because we need, like, teams. Teams. I should be getting the team data. Right? And I should be getting that. Teams. Why am I not getting that data that I want? Let's try it again. Send flows. Send announcement. We're gonna take a look at this. Okay. Okay. Team.team.marketing. And then can I get through users from there? Teams.starTeam.star. Team Members Star. Love waiting through a good data model here. Let's see what we get now. Test, flows, send announcement. Okay. So now we're getting closer. Right? Here's our team. There's our members. We got the users. Those are what we're trying to get to. Teams.user.star. This is gonna give me the emails of the team members. Once I get that, send announcement. Flows. Is this gonna give us the data of what we want? Yes. There we go. Alright. And, really, all I need here is the email. This is where I'm going to turn to AI for, like, three minutes worth here. Let's just open cursor. Write a function, a JS function that returns the array of emails for this sample data. There we go. First, we need to okay. Spit something out, friend. Data dot teams, that flat map. Okay. So there's a simple this is writing TypeScript, which we're not gonna be able to use in flows, but you do have the ability to run just JavaScript. We'll call this transform. Let's just pop in that function. We'll remove the types. Data data dot teams. Alright. So we're gonna do this. We're going to get the, what did we call that last one? This is just announcement. We're gonna get the announcement data. So here's what we're gonna do. We'll do, this doesn't need to be async. We're gonna say const, actually, we're just gonna return get emails, and the data is gonna be dot data dot announcement. So as you go through a flow, like, each step will append the result under the key. So this data object that we pass, I can access the announcement data for that step through this. And then the final step here is gonna be sending an email, which I don't think I've actually I can't remember if I got emails hooked up or not. We're gonna get this transform. We're going to reference that. So I'm gonna send the email. We're going to say transform, transform. And I'm gonna make sure that that is that should be passing an array. So we're just gonna use the raw value. I don't have a this is an important announcement. And then for the WYSIWYG content, we are going to do announcements dot text. Alright. We got forty five seconds. We're only gonna get one go at this probably. Let's see if this is actually gonna work. Run this flow. Send announcement. Is it actually going to run the flow to brian@directus.io? Did I configure this? Yes or no? Come on, baby. Come on. It shows my email is disconnected. Gmail. Come on. Come on, Gmail. Fifteen seconds. Are we gonna get an announcement? Are we gonna get an announcement? Are we gonna get an announcement? No. I don't think I've got email connected inside this actual Docker Compose file, to be honest with you. Too bad. Too bad. We didn't get to the last one. That sucks. Sending messages to the entire team, it should go through. And if I were on a Directus cloud instance, it probably would go through, but I don't have the actual email drivers conf like, the config set up for that. So, like, no emails are going through there. You know, obviously, I would wanna take the time as a next step to just go ahead and do that. So, if I go to, like, the direct us documentation and I look for email, you know, I would want to set up, like, my either my SMTP settings or Mailgun or SES, to actually send those emails out. But I will I I don't know if we're gonna call this a win or not because it did run this flow. It did give us the emails. We're not getting any errors there. We're just, yeah. I don't know. Let's let's just do the explosion anyway. That's my favorite animation of this whole series. So that is building a company Internet internal portal. Hope this was a helpful look at just how powerful Directus is. I will catch you on the next episode of 100 apps, one hundred hours.","6926779e-6ca8-4901-89ee-7e1b754fc47b",[663],"a8a10989-2021-40d3-b730-b1111150fcf1",[],{"id":142,"number":143,"show":122,"year":144,"episodes":666},[146,147,148,149,150,151,152,153,154,155],{"id":154,"slug":668,"vimeo_id":669,"description":670,"tile":671,"length":643,"resources":8,"people":8,"episode_number":341,"published":556,"title":672,"video_transcript_html":673,"video_transcript_text":674,"content":8,"seo":675,"status":130,"episode_people":676,"recommendations":678,"season":679},"ai-personalized-landing","1059438000","Watch Bryant create a system that generates custom landing pages for target companies by scraping their websites and using AI to craft personalized sales pitches. With a mix of Directus hooks, FireCrawl for content scraping, and the Anthropic API for copywriting, he builds a complete workflow that transforms company data into engaging sales pages for his fictional developer seat cushion business.","8b694159-4108-4801-8254-3a42720143fe","Mission: AI Personalized Landing Page","\u003Cp>Speaker 0: Alright. Alright. Welcome back to, yet another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. And in this episode, we're reaching back into the AI personalization bucket, and we're gonna build some personalized landing pages.\u003C/p>\u003Cp>If this is your first time on 100 apps one hundred hours, there are only two rules to the show. We have sixty minutes to plan and build an application, a clone of one of your favorite apps, or some totally random idea. No more, no less. And then number two is use whatever we have at our disposal. Could be past projects, could be AI, could be all sorts of crazy things that we might get into today.\u003C/p>\u003Cp>I've got no idea whether we're actually going to be able to, like, push this one across the finish line or not. So that should be exciting for this episode. Alright. So AI personalized landing pages. You know, there's another episode we've done in this season, like, content personalization.\u003C/p>\u003Cp>This time around, I wanna get very, very specific and say, hey, I have a target account or a a company that I wanna do business with. I wanna use AI to generate a landing page for them. So let's dive right in. We're gonna put sixty minutes on the clock, and let's go. Here we are.\u003C/p>\u003Cp>Alright. So I always like to start these by just discussing what are the features that we actually want out of this. Right? So as far as our functionality, we'll just make this freaking huge. Oh, not that big.\u003C/p>\u003Cp>Alright. So as far as the functionality we want out of this, here's how I'm thinking this is actually going to flow. We're going to add an organization to our back end, which is gonna be powered by Directus. And then a the AI is going to scrape the website. We want to use that content to generate and use content found to shift return.\u003C/p>\u003Cp>There we go. That's what I needed to generate a personalized landing page for that specific org. Okay? Display that using a front end. That's it.\u003C/p>\u003Cp>I would love to be able to do this on the fly, this part of it. Right? But, unfortunately, like, the LLM models are not super fast or well, if you use something like GROC, maybe. But the this step is gonna have to be done asynchronously. So we'll sort through that.\u003C/p>\u003Cp>Let's think through, like, our our actual data model now. So let's throw this up on the board. We're gonna have organization. And, honestly, like, I I could have a separate one for pages. You know, we've got, like, the page builder set up inside Directus, which is really nice.\u003C/p>\u003Cp>Something that we've showed in previous episodes where I can build a dynamic page based on predefined blocks. In this case, we might just keep it super simple. We've got an organization that I wanna target. Within that, we're gonna have a name. I need a website URL to scrape.\u003C/p>\u003Cp>And then the rest of it would be generated. Right? You know, let's call it blocks, I guess, generated blocks. Maybe we got a hero. I wanna have, like, a feature section.\u003C/p>\u003Cp>Call to action. Call to action, CTA. Man, I have outgrown my box here. We'll just add that up there. Alright.\u003C/p>\u003Cp>So this seems pretty solid, I guess. I what's our plan of attack? Right? What are we going to sell? What are we going to pitch?\u003C/p>\u003Cp>You know, what is our organization that we are setting up? Alright. So, as far as what I've got in the can already, I've got a Nuxt application. I've got a direct instance. So if we load up local host 3,000, I should see my Nuxt instance, my Nuxt application, just the same starter I use in all of these.\u003C/p>\u003Cp>And if I go to Logos eight zero five five, I should see a blank directus instance. That is not the one that I wanted to see. Let's control some of these Docker containers here, and then we'll fire this back up to see if we get what we want. Boop boop boop. Should be a blank instance.\u003C/p>\u003Cp>Great. Okay. Alright. So now I'm gonna log into this blank Directus instance. Directus is gonna give me all the back end functionality that I want and or need.\u003C/p>\u003Cp>And you could see it is totally blank. Right? So let's attack this from the back end perspective first, and then we'll switch to the front end, and then we'll switch to the back end, and we'll switch back and forth and back and forth until we all get confused with how many tabs I have open. Alright. Create a new collection.\u003C/p>\u003Cp>So let's create a new collection for our organizations. Right? As far as table names go, I prefer plural. Some people prefer singular. Totally up to you.\u003C/p>\u003Cp>We're just gonna add some of these specific default fields that Directus makes available to us. Like, hey. Is this published? Is there a sort order to this? Obviously, date created here will be populated with a timestamp of when this record gets created.\u003C/p>\u003Cp>Great. So now we have an organization. I'm gonna make this look very pretty. We will call it we'll search for maybe business. Forget I'm talking to myself here.\u003C/p>\u003Cp>I was about to ask you a question of which icon to use, but, still just talking to myself. Alright. So as far as our first field, right, we're going to add a the name of the organization. Pretty simple. Let's keep editing.\u003C/p>\u003Cp>I can make this shorter. We'll just go half width. So I am controlling the field, like the the actual form that we'll be interacting with as well as my data model and my APIs. So next, we will save this and let's add a website or URL. Let's just call it website.\u003C/p>\u003Cp>And what are we gonna do here? Let's go to advanced field creation. We could see that this is allowing a null value, but let's make sure that we set that to required. So when I fill out this form, it is going to make me input that. Now if I keep this as nullable, that means we could still potentially pass that value into the database as null, using the API.\u003C/p>\u003Cp>I could turn that off if I wanted to. I could also make sure this is unique as well, so that we don't generate this a million times. And for the display, we'll just show the raw value. Right? Is that it?\u003C/p>\u003Cp>Let's add a little icon just to make this a little fancy. This will be a link. Great. And now we have a website. And let's go ahead and make name required as well.\u003C/p>\u003Cp>We're gonna fill both of those things out. Boom. Amazing. I can go in. I can now add a name like Directus and a website, Directus.io.\u003C/p>\u003Cp>Save. Great. Notice that perfect little icon right there. It looks great. Right?\u003C/p>\u003Cp>I can even oh, no. I was gonna say I could point to it. I can point to it. There it is. Alright.\u003C/p>\u003Cp>So now we have this, like, data model. It's amazing. There's two fields here. This is really underwhelming as Bryant. Alright.\u003C/p>\u003Cp>So we've got an organization in our back end. Let's switch round to the front end, and let's start, like, fleshing something out. And this is where I will say, like, POC generation, AI has got an incredibly great for this, especially with, like, UI libraries like Shad CN and Tailwind for styling. It's gotten really good at creating components and things like that. So, what are we going to what are we gonna generate here?\u003C/p>\u003Cp>Like, what are we pitching? What are we what are we selling? Coffee mugs. Let's do something super developy developer ish. Maybe like a a seat cushion.\u003C/p>\u003Cp>Maybe we're gonna sell seat cushions. I'm hopefully, I look younger than I feel, but I've got three little girls. I've got a back that aches every day when I wake up. So maybe that's what we'll do here. We're gonna generate some seat cushions.\u003C/p>\u003Cp>Alright. So, when I think of the front end here, let's create a page. So let's create a new route for the org. Organization. I'm gonna wrap this in brackets there.\u003C/p>\u003Cp>Great. We'll do v base setup, script setup. Let's change that to length. Yes. And this is giant text, so that's a good thing.\u003C/p>\u003Cp>Alright. We got an organization. We're gonna fetch that organizational data. And behind the scenes, I've used this in so many videos. I'd never take the time to explain it.\u003C/p>\u003Cp>Basically, I'm using the Directus SDK. I've just got a quick little plug in as far as Nuxt goes. We're gonna pull the direct as URL from the configuration. We're gonna create a really simple direct as client, and we're gonna provide that to the Nuxt application. So the first time this application spins up or, you know, when it loads, it creates that instance for us and makes it globally available through something like this where we could say, Directus use Nuxt app.\u003C/p>\u003Cp>The other thing that I'm gonna do here, I'm gonna import read items from the Directus SDK and then we're gonna fetch this data. So we'll say data, that's gonna be organization. No thank you AI. Wait. We're gonna use the async data composable from Nuxt, just a handy little helper for caching.\u003C/p>\u003Cp>We're gonna call this organization organization. We're gonna get that organization from our routes, route.params.organization. No. We don't want that. We're going to do it this way, route.params.organization.\u003C/p>\u003Cp>We're gonna do direct us dot request. Yes. Direct us request. Read items, and we're just gonna wrap that. And do I have some nice formatting?\u003C/p>\u003Cp>Great. We got some formatting. Okay. So what are we doing here? We're looking for a slug.\u003C/p>\u003Cp>We don't really have a slug set up yet. So let's just do an ID. And maybe we do wanna set up a slug, maybe we don't. Route dot params ID. You'll notice this filter syntax here.\u003C/p>\u003Cp>Directus has a ton of different filter rules that I can use to create deeply nested or crazy queries where I can fetch everything I need in a single API call. And then I'm just gonna go in and log this out on the page. Is this possible? Let's go into Directus and just make this process easy for me as well. There is a button links option here which is just presentational.\u003C/p>\u003Cp>And we're gonna say view on website. Give it a label, arrow up link. Do it is there like an external link? Arrow. Let's just go arrow caret ray.\u003C/p>\u003Cp>Okay. And we're gonna say HTTP, we want the URL local host 3,000 is what we're running on. And then we're gonna run this at the ID. Great. Well, it's safe.\u003C/p>\u003Cp>Now I should get a button on this page where I can view on the website. And lo and behold, it looks amazing. This is the most beautiful page ever. We're just gonna go back in and edit our permissions. So by default, Directus keeps everything secure, keeps a tight lid on everything.\u003C/p>\u003Cp>So this public policy that we have doesn't have any access to our data. So let's go in and add read access for organizations and let's let's set files as well in case we decide to upload some pictures of our amazing seat cushions that we're going to sell. And now if I refresh the page, we could see, great, I have a name, I have a website. Alright. That's kinda underwhelming.\u003C/p>\u003Cp>Not great. So let's have it design. Let's retray, here. We are going to create I'm gonna use Claude's sonnet, and let's create a landing page. You are let's let's back up a minute.\u003C/p>\u003Cp>What do we want our landing page to look like actually? Right? So we probably got like a hero. We've got a features for our seat cushions, seat cushions, hero section. And then we've got like a like a buy button.\u003C/p>\u003Cp>Buy button. Buy now. CTA. Alright. So that's gonna be the structure for our page.\u003C/p>\u003Cp>Here we go. We are going to flesh this out. You know, we probably want to you are are we gonna, like, one shot this? Let's see. Well, if I do this, you are a expert I I don't think you really need to do this because it's picking up up whatever I have, but create a hero section component for a online store that sells seat cushions to developers.\u003C/p>\u003Cp>A beautiful, well designed view three component using alright. So one of the features I've been experimenting with in cursor is this documentation feature. This starter has like the Nuxt UI library built into it. Which version am I actually running? Did I upgrade this to the latest one?\u003C/p>\u003Cp>Yeah. I'm running the alpha version. So we're gonna use those docs and tell when CSS. We'll see what we come up with here. Let's create a beautiful new component.\u003C/p>\u003Cp>You page hero. I don't know I don't know that I have any of these actually. Just you don't use our Nuxt UI components after all. Okay. So we can see what is coming up with hero section.\u003C/p>\u003Cp>We got a feature, ergonomic design. Yeah. I'm kinda curious to see what it comes up with as far as, like, the data model around this as well. We'll just go ahead and flush this out for it here while component section, features. Oh, this is running kinda slow.\u003C/p>\u003Cp>New collection, blah blah blah, right column. Yeah. It would probably even be easier just to go into, like, Tailwind UI and find some of these components here. They've got some nice ecommerce components. Let's look at it like a hero section.\u003C/p>\u003Cp>Alright. Let's just we'll just see what it came up with first and accept this. We'll go back to our organization. We'll insert this component in here. This is our hero section.\u003C/p>\u003Cp>Great. Cushion. JPEG. Is there a cushion dot JPEG? Hero section.\u003C/p>\u003Cp>Where is this cushion dot jpeg cushion hero dot jpeg. Alright. Let's bring in one of our first AI big guns here. We'll go to replicate, and I'm just gonna use our flux model here. It will create a product image of a futuristic seat cushion for developers if I could actually spell, that'd be dangerous.\u003C/p>\u003Cp>Developer's seat cushion, developer's desk chair. I don't think punctuation is hugely important here. Let's see what we come up with. For 6¢ an image. What are we gonna get out of this?\u003C/p>\u003Cp>That looks pretty pretty wicked awesome there. Right? So we're gonna download that. Let's dump this into our assets folder, and we'll call it seat cushion dot JPEG, and we'll just update that here. Alright.\u003C/p>\u003Cp>So this should be assets, images slash seat cushion. And oh, what have we got here? Something weird is going on. Better comfort for better code. There we go.\u003C/p>\u003Cp>Alright. Now we're getting somewhere, except this looks really odd with our price badge. Something's going on there. Let's just comment that out. There we go.\u003C/p>\u003Cp>We got our seat cushion. This is great. Alright. Now let's have it designed something else. Right?\u003C/p>\u003Cp>Let me just shrink this down just a little bit. Feature alright. So we want this to be our headline. Right? So let's flush this out a little bit more.\u003C/p>\u003Cp>This is gonna be our headline. This will be our description. I'm just trying to think through like our what our props are gonna look like for this. Define props, features as a feature. Great.\u003C/p>\u003Cp>Learn more, shop now. Better comfort for better code. This is a new collection. Badge. Alright.\u003C/p>\u003Cp>So we'll just stick that in our props. Now we got badge, got features. Do we actually want the icons we can. I think I've got the Nux icon library built into this so we could simplify this, like hero icons. There we go.\u003C/p>\u003Cp>I don't know what other hero cons we have here. Shopping bag, check circle. There we go. And then we'll, like, flesh this out. Where is that icon at?\u003C/p>\u003Cp>Icon. Feature dot icon. Yeah. So we could ditch all of this and make this a little bit cleaner. We'll keep that.\u003C/p>\u003Cp>Right? You icon. Name equals feature dot icon. We'll just ditch this, ditch that, clean this up a bit, and we should still have our icons. Great.\u003C/p>\u003Cp>And then we're gonna move out features out of here, we'll put that into this. So for now, we'll just say here's our data, our data. We'll drop our features here. Sometimes it is frustrating as I'll get out to properly just navigate using these AI tools. The tab tab tab is it's nice when it works well.\u003C/p>\u003Cp>When you have all these extensions included, it does get a little bit messy. Let's check on time. Where is Figma? Thirty seven minutes. Alright.\u003C/p>\u003Cp>Better get to it, g. Alright. If I hit refresh, are we actually going to let's call this data, and then we'll go back into our hero section. We'll just wrap all this up into data. There we go.\u003C/p>\u003Cp>There we go. Data dot badge, data dot headline, shop now, learn more, data dot features. Okay. So maybe that is a little long as far as a headline. What do we have previously?\u003C/p>\u003Cp>Yeah. Alright. Comfort for better code. There we go. Okay.\u003C/p>\u003Cp>Now, this is our data. Right? This is our data structure. Maybe we tell this to generate. Alright.\u003C/p>\u003Cp>So how are we gonna generate the actual code here? Like, how are we gonna generate the personalization of this specific page? Let's just ditch the code part. There's our website. Looks great.\u003C/p>\u003Cp>How are we gonna personalize this? So for that, let's kick off a Directus extension, basically. Basically. Yeah. Nothing basic about it.\u003C/p>\u003Cp>So what I'm thinking we'll do here, we'll set up a custom Directus extension so that whenever I am adding a new organization, it will create that data for us. So let's also do this where we'll go in and we'll call this just page data. Page data. Page content. It doesn't really matter what we call it.\u003C/p>\u003Cp>I'm just gonna set it to be JSON. That's what we're gonna ask back from the API. Alright. Let's kick off this extension. So I'm inside my Directus directory.\u003C/p>\u003Cp>We're probably gonna go into CD extensions within that. And then I'm gonna go n p x create direct us or create dash direct us dash extension latest. And I am going to create a let's do a custom hook. This is generate personalized page, custom landing page, AI landing page. We use TypeScripts.\u003C/p>\u003Cp>We'll go ahead and install the dependencies. And one of the things that I've got set up here inside my Docker Compose for this, obviously, like, I'm I'm I'm creating a volume for the extensions here so we could pull those in. But I'm also somewhere in here. I have extensions auto reload set so that whenever we build this extension, Directus is going to look for that and reload this extension or at least it should. So we'll go into CD AI landing page, npm dev.\u003C/p>\u003Cp>Alright. So let's actually open this bad boy up and see what we've got here. Items dot create. Okay. So now I'm just quickly going to cheat a bit and pull up the direct to stocks.\u003C/p>\u003Cp>Hooks, custom API hooks. So what we're looking for here is after this item is created, and I think the structure for a collection itself is a collection name dot items dot create. This is what we're gonna do. So our filter will look something like the or our action here will look something like this. We don't necessarily need this.\u003C/p>\u003Cp>Alright. So we're gonna say organizations dot items dot create, organization created, and this will be async. And what this is going to receive should be alright. We're gonna get the context here. We can see what that context is gonna look like.\u003C/p>\u003Cp>That's how we're gonna initiate some services. What does the action receive? Right? The action receives the event name and a callback function that is executed whenever the thing is submitted. Alright.\u003C/p>\u003Cp>Extensions guys. Do we have one on hooks? Use hooks to validate phone numbers. Items that create input collection. So we're gonna get the payload, I'm assuming, and the content, item context.\u003C/p>\u003Cp>Let's see just kinda what we get here. Extension's changed. Okay. Test httpstest.co. If I check my Docker logs, I cannot import.\u003C/p>\u003Cp>Let's just stop and spin up the Docker container one more time. Make sure we're actually getting that. Okay. I could see that extension being initialized. Oh my gosh.\u003C/p>\u003Cp>That gets ugly, doesn't it? Alright. Let's try this one more time. We'll delete testco, test Co. Are we seeing any console log?\u003C/p>\u003Cp>I'm not seeing any logs here. Async, define hook. Extensions reloaded. Organization oh, duh. You gotta use the right right syntax, Brian.\u003C/p>\u003Cp>Frustrating to be under the clock. Testtest.co. Hit save. Okay. So now we're seeing some stuff here.\u003C/p>\u003Cp>Great. Alright. There's our holy moly. Alright. So that is our there's the context that we're getting there.\u003C/p>\u003Cp>Organization created. There's the payload that we're receiving. And then we're getting the item context. Okay. Great.\u003C/p>\u003Cp>Alright. So what we're gonna do, what do we want to actually happen when this is going to pop off? There's a third party tool that we're gonna use for this called FireCrawl that I've been testing. Right? So let's try this.\u003C/p>\u003Cp>We're going to add Firecrawl. Js. Alright. So we're gonna go to our extension that we're running. I'm gonna install this library from FireCrawl.\u003C/p>\u003Cp>And we're gonna go ahead and one of the other things that I'm gonna do, we're gonna call this the organization's service. We're going to extract that out of the context. Const request. No. It's gonna be the services out of the context, then the item service through the services.\u003C/p>\u003Cp>And we're gonna call new item service. Great. And this is gonna give us the ability to update. Right? So we're gonna initialize that service.\u003C/p>\u003Cp>We've got that installed. Now let's copy our code here. I'm hoping this is my live API. New FireCrawl app. We need to import that.\u003C/p>\u003Cp>Scrape a single web page. Got it. We'll move that down below our item service, and we're gonna get the scrape result. And for now, let's just oh, I can already see the typo happening here. That's gonna be frustrating for me.\u003C/p>\u003Cp>And we're gonna say await updated org equals await organizationservice.update.one. Data, does that need to be wrapped in data? I don't know. Scrape content. Alright.\u003C/p>\u003Cp>We're gonna return the updated org, and let's wrap this in a try catch as well. Try catch. Console dot error. See if we get some formatting. Nice.\u003C/p>\u003Cp>Okay. Let's fire up the dev server for this and cross our fingers. Right? The other thing I need to update here, we're gonna have to go in and figure out which tab has all of my browsers in it. Maybe we just move that to a single browser for now.\u003C/p>\u003Cp>And we're gonna add a new field for this. We're gonna call it, scraped result. Scraped result. I don't know what this is gonna be. I'm assuming scraped content.\u003C/p>\u003Cp>I'm assuming this is gonna be JSON data that we're gonna get back. I guess I could look and see. Success data. You know, we could also just store the markdown. That might be easier.\u003C/p>\u003Cp>Where do we go? Scrape content. Let's delete that. Where does this store the markdown? Scraped.\u003C/p>\u003Cp>Scraped content. Alright. So according to FireCrawl, if we do this correctly, we're gonna get something like this. So with the scrape result dot data dot markdown. Fingers crossed.\u003C/p>\u003Cp>That's what we're gonna get. We could also just console log that to verify scrape content, scrape result. Alright. Let's go for it and see one last thing that I wanna do here. This is not, using, like, any of our accountability, so do not do this in production.\u003C/p>\u003Cp>But we'll just allow the public to create and update all the content on the underlying instance. Great. Love how secure I am being. I'm sure everybody on my team would be thrilled. Alright.\u003C/p>\u003Cp>So let's just go to Directus as the example. HTTPS, Directus.io. That's our website. Oh, I've already got directis.io hard coded in there as well, so we're probably gonna want to pick that up out of the payload as well. Can we still see that payload?\u003C/p>\u003Cp>No. Let's do this. We'll see the payload.website URL. We'll see if this actually works. Fingers crossed that it does.\u003C/p>\u003Cp>Scrape content. Save. What happens on this side of it? Around URL must be a top level domain or valid path. So I'm getting that error there.\u003C/p>\u003Cp>It's fine. Should set this up on update or create. Directus.io. Save and stay. Do we see what we get back?\u003C/p>\u003Cp>Is this thing still running in the background? What are we doing here? I got no visibility into what's happening here. Let's try it again. Directus.\u003C/p>\u003Cp>Directus. Io. Hit save. Must be a failed to scrape URL. URL must oh, duh, dummy.\u003C/p>\u003Cp>This is where it helps to define types so you don't make these same silly mistakes as I have. So I've called this field website. I was calling it website URL here, so not actually passing any website. Cannot read properties of collections. So there's our scrape results.\u003C/p>\u003Cp>Do we see the markdown? There's our markdown. Cannot update one. What am I doing wrong here? We're not updating the schema.\u003C/p>\u003Cp>We need to extension service, access items. Yes. Here we go. This is what I actually need to see. Alright.\u003C/p>\u003Cp>So we got our context. We're gonna do services get schema. We need that schema. And we're also gonna need the accountability schema. So in our item service definition, we don't wanna do this.\u003C/p>\u003Cp>We're gonna do this. Equals await. No AI. No. We're gonna await git schema.\u003C/p>\u003Cp>So we're gonna get the schema from our context and on the accountability side, accountability. That'll be accountability. If we just destructure that, I think that gives us what we need. Let's go back and try it again. Love hacking this stuff together on the fly.\u003C/p>\u003Cp>No pressure. Directus directus dot I o. Hit save and stay. Let's just watch over here and see what happens. Fire crawl seems to take a little bit, so that is forbidden.\u003C/p>\u003Cp>I don't know what we're getting over here. Can I just leave off the accountability object? How many times am I gonna create this before I just do update? Directus Directus dot I o. Save.\u003C/p>\u003Cp>Probably burning through these fire crawl credits as we speak as well. That that that that that, forbidden. You don't have permission to access this. Why is that not the case? What is the alright.\u003C/p>\u003Cp>So let's stop loading that. Let's just say console dot log the payload. Actually, I think it's I goofed up, didn't I? What are we gonna be receiving here? It's not gonna be it'll be payload dot key.\u003C/p>\u003Cp>Payload dot let's just log the whole payload again and see. Just comment out this. Comment out. Directus Directus dot I o slash payload key. Okay.\u003C/p>\u003Cp>Duh. You're smarter than that, Brian. Alright. So we need this key. That's what we're getting is the key payload dot key.\u003C/p>\u003Cp>So here, payload dot key. There's the event. There's the key. There's the organization of the collection we already got. Alright.\u003C/p>\u003Cp>Let's keep rolling and maybe try to actually complete this one. Directus. Directus dot I o. Save. Can we actually get the scrape content?\u003C/p>\u003Cp>Can we actually complete this on time? Can we do any of these things in any sort of actual working fashion? Is this actually gonna work? Will we know? How do I know?\u003C/p>\u003Cp>Console.logupdatedorg. Directus. Directus I o. Save. There's the payload.\u003C/p>\u003Cp>Fire crawl really takes a bit. Status code, 200. Updated org. Is this actually gonna see what we want? No.\u003C/p>\u003Cp>Of course not. It's not gonna update the right one. Data dot scraped content. I'm sure this is what we need to do here. Right?\u003C/p>\u003Cp>Update one. Let's look at the services. Update an item. Yep. It's not inside data.\u003C/p>\u003Cp>So I don't need that to wrap. Gosh. I'm just eating up all the time. I wish I spent more time with hook extensions. Super powerful because you're basically just updating the underlying Express app instance anyway.\u003C/p>\u003Cp>But alright. So now let's reach for anthropic. If we hit refresh, do we ah, still not getting the data that I want. Right? It's saying that it's updated that org.\u003C/p>\u003Cp>Scrape result. Scrape result scrape content. So they tricked me. It's not it's not as it shows. It's actually not there's actually no data attribute there.\u003C/p>\u003Cp>It's basically just scrape result dot markdown. Alright. Last freaking time for the scraped content, Directus Directus dot I o. I was not planning on doing this much troubleshooting. Behind the scenes here, I'm gonna go in and just create a API key for anthropic.\u003C/p>\u003Cp>Hundred apps. There's my API key. Updated org. Did we actually get what we want? Holy moly.\u003C/p>\u003Cp>We got some scraped content in here. Okay. Alright. So now the next step in the puzzle is going to be call the Anthropic API to generate a landing page for the organization with the JSON schema, page data. Alright.\u003C/p>\u003Cp>So now this is one of the things where I've been leaning on the Vercel API, or AI SDK. So I'm just gonna install it here, p m p I. This is just AI. And they've got a way. The last time I checked, the Anthropic does really well.\u003C/p>\u003Cp>Like, SONNET does really well at copywriting. But it doesn't have the JSON mode that the ones like OpenAI have. So I'm just gonna go in and look for provider management providers. Where are my providers? Providers and models.\u003C/p>\u003Cp>Looks for the anthropic provider. I'm gonna add it as well. And let's see what we can get here. Import anthropic. So I'm gonna import this guy.\u003C/p>\u003Cp>And then we're also going to do what? Create anthropic. We'll do that here. Now what I would normally do is this context object should have an e and v variables as well. API key.\u003C/p>\u003Cp>I'm not gonna do that here because I am worthless, basically. No. We're eating up a lot of time. Base URL, API key, generate text. We want to generate objects.\u003C/p>\u003Cp>So there's our anthropic client that we're creating. Don't mind that API key. Now let's define what do we need? We need Zod as well, p m Zod. And we're going to use was it generate object in here?\u003C/p>\u003Cp>See if we can find it. Generate object. Alright. Generate object from AI. Okay?\u003C/p>\u003Cp>Now what we're trying to do is just basically, we're gonna define a Zod schema of the object that we want, and that's gonna be our hero section. Right? Zod schema. We're gonna lean on the props. There we go.\u003C/p>\u003Cp>Okay. So now I'm just gonna copy this wholesale. We're gonna stick this in our custom hook. Again, not being careful at all about separation of concerns or cleanup. So there's our Zod schema.\u003C/p>\u003Cp>We want the object to return hero section schema. Alright. So now let's look up what we actually want this to how we're gonna work this. Generate object. We don't want testing.\u003C/p>\u003Cp>There's gotta be a better during text, during structured data. Alright. So our model okay. So we want the object equals await generate object. I'm hoping this is right.\u003C/p>\u003Cp>Fingers crossed with AI. Generate a landing page for the following organization with the content. Okay. For the organization payload. Name, the following content.\u003C/p>\u003Cp>You are an expert copywriter, and your job is to generate amazing landing pages that are personalized for our target clients. We sell the best desk chair cushions in the world for developers. Schema. There's our schema. That's our Zod model.\u003C/p>\u003Cp>Okay. When we get that back, what are we gonna do with the object? We are going to update the org, updated org two. It's fancy, fancy, fancy. Okay.\u003C/p>\u003Cp>Alright. How we doing on time? We got nine minutes to try and figure this out. Let's just PM, PM, dev this bad boy up and see if this is actually gonna come out at all. We're rebuilding this extension.\u003C/p>\u003Cp>Dun dun dun dun dun. I'm gonna nuke this entirely. Let's try it again. Let's see if it works for directus.io. We hit save.\u003C/p>\u003Cp>I could check the logs on Docker. Create anthropic is not defined. Anthropic. Response. Anthropic.\u003C/p>\u003Cp>Where is that? Providers and models. Anthropic provider. Create anthropic. Import the default provider.\u003C/p>\u003Cp>Oh, no. We could just import the what am I doing? Create create anthropic. Organizations. Oh, no.\u003C/p>\u003Cp>Directus. Duh. Directus .I o. Hit save. There's the payload.\u003C/p>\u003Cp>Obviously, it would be great to have visibility into this. And one tool that I've been using as of late is ingest. Great tool for something like this, generated hero section. This is gonna be nice, doc. Okay.\u003C/p>\u003Cp>Alright. So now before we reveal what it generated, let's just try to make sure this is actually gonna work. So instead of the data here, we're just gonna use the organization dot data dot page data. Organization dot page data. Generated hero section.\u003C/p>\u003Cp>That looks nice. Organization dot page data. Dun dun dun, the big reveal. Close that guy out. No.\u003C/p>\u003Cp>Status message. What is the status message? Did we actually get the page data generated? Okay. We got the page data generated.\u003C/p>\u003Cp>It looks like it's coming back with data here as well. So we should probably fix that in our hook. Page data page data is what? Object dot data. But for now, what I'm gonna do, I'm gonna just strip this out.\u003C/p>\u003Cp>Right? Great. And we refresh. We refresh. We refresh.\u003C/p>\u003Cp>No. What is going on? Organization. I'm excited for this, and I'm hoping we could get this to work. Right?\u003C/p>\u003Cp>Why am I not getting any of my data? Organizations. Access policies. We can see that organize oh, that's because it's not the right organization. Alright.\u003C/p>\u003Cp>I was using the old organization. We blew that one away like a hundred times. Right? This is probably where you'd wanna slug. Five minutes left.\u003C/p>\u003Cp>What do we have on the clock, Johnny? No. It's not working. Boy. Alright.\u003C/p>\u003Cp>There's our page data, organization dot page data. There's the scrape content. Why are we not seeing that? Right? Organization.\u003C/p>\u003Cp>Let's try it now. Status code. What is the error that we're receiving? V if organization dot page data. I could see the page data, man.\u003C/p>\u003Cp>Organization. Oh, duh. Gosh, man. What an idiot. Alright.\u003C/p>\u003Cp>So I I've done this on another episode as well. We're gonna transform this to get data. Where is this at? Is it here? Transform data.\u003C/p>\u003Cp>I don't know if this is how it functions. Error results, organization. I'm not sure the exact syntax, and I'm not gonna mess around with it. So we're gonna do this. It's gonna be organization.\u003C/p>\u003Cp>Boom. Bam. Directus plus Comfy Desk revolutionize your development experience, Seamless integration. Integrate Directus' flexible back end with your favorite front end while sitting comfortably on comfy desk cushions. Tailor your with adjustable cushions.\u003C/p>\u003Cp>Perfect. Alright. So now we have landing pages that are personalized for a specific client within, like an AI workflow. Right? So let's try this with some other company.\u003C/p>\u003Cp>Let's say OpenAI. I guess just to to pit these guys against each other here is this OpenAI.com. OpenAI Com. We'll hit save. And hopefully, this all runs, works nicely on the back end.\u003C/p>\u003Cp>There's the scrape result. You know, if I wanted to get really fancy, obviously, I could take this to the extreme here. But let's look at OpenAI. We'll view this on the website. What is going on?\u003C/p>\u003Cp>For noose proper next string, OpenAI. Okay. This one is not super strong. Right? Not sure what the scrape data result was.\u003C/p>\u003Cp>It looks like Firecrawl didn't do a great job with their website. Right? Let's try Apple.apple.com. Save. And we'll see what it comes back with.\u003C/p>\u003Cp>There's the scrape content. And, again, this is just coming back with markdown. You know, I might have better luck with, like, actual JavaScript parsing or something like that. But boom. Elevate your comfort coding experience with Apple inspired comfort, the iCushion Pro.\u003C/p>\u003Cp>Anyway, that's it for this episode. We've achieved this. I'm pushing a stop button on it. One minute left, we have basically fleshed out this personalized landing page where we add an organization, we use AI to scrape it, we then we generate content using LLMs, based on a specific schema, and then boom, we've got a personalized landing page that we could share with them. As far as next steps for me would probably be, like, setting this up on, like, a pretty URL or a slug, you know, adding this to some type of outbound, like, outreach campaign potentially.\u003C/p>\u003Cp>That's it though. It's amazing what you can put together in just an hour with tools like Cursor, Directus, Nuxt, Anthropic, all these different things come together to create stuff that's fun to build. I'll catch you on the next episode. Peace.\u003C/p>","Alright. Alright. Welcome back to, yet another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie. And in this episode, we're reaching back into the AI personalization bucket, and we're gonna build some personalized landing pages. If this is your first time on 100 apps one hundred hours, there are only two rules to the show. We have sixty minutes to plan and build an application, a clone of one of your favorite apps, or some totally random idea. No more, no less. And then number two is use whatever we have at our disposal. Could be past projects, could be AI, could be all sorts of crazy things that we might get into today. I've got no idea whether we're actually going to be able to, like, push this one across the finish line or not. So that should be exciting for this episode. Alright. So AI personalized landing pages. You know, there's another episode we've done in this season, like, content personalization. This time around, I wanna get very, very specific and say, hey, I have a target account or a a company that I wanna do business with. I wanna use AI to generate a landing page for them. So let's dive right in. We're gonna put sixty minutes on the clock, and let's go. Here we are. Alright. So I always like to start these by just discussing what are the features that we actually want out of this. Right? So as far as our functionality, we'll just make this freaking huge. Oh, not that big. Alright. So as far as the functionality we want out of this, here's how I'm thinking this is actually going to flow. We're going to add an organization to our back end, which is gonna be powered by Directus. And then a the AI is going to scrape the website. We want to use that content to generate and use content found to shift return. There we go. That's what I needed to generate a personalized landing page for that specific org. Okay? Display that using a front end. That's it. I would love to be able to do this on the fly, this part of it. Right? But, unfortunately, like, the LLM models are not super fast or well, if you use something like GROC, maybe. But the this step is gonna have to be done asynchronously. So we'll sort through that. Let's think through, like, our our actual data model now. So let's throw this up on the board. We're gonna have organization. And, honestly, like, I I could have a separate one for pages. You know, we've got, like, the page builder set up inside Directus, which is really nice. Something that we've showed in previous episodes where I can build a dynamic page based on predefined blocks. In this case, we might just keep it super simple. We've got an organization that I wanna target. Within that, we're gonna have a name. I need a website URL to scrape. And then the rest of it would be generated. Right? You know, let's call it blocks, I guess, generated blocks. Maybe we got a hero. I wanna have, like, a feature section. Call to action. Call to action, CTA. Man, I have outgrown my box here. We'll just add that up there. Alright. So this seems pretty solid, I guess. I what's our plan of attack? Right? What are we going to sell? What are we going to pitch? You know, what is our organization that we are setting up? Alright. So, as far as what I've got in the can already, I've got a Nuxt application. I've got a direct instance. So if we load up local host 3,000, I should see my Nuxt instance, my Nuxt application, just the same starter I use in all of these. And if I go to Logos eight zero five five, I should see a blank directus instance. That is not the one that I wanted to see. Let's control some of these Docker containers here, and then we'll fire this back up to see if we get what we want. Boop boop boop. Should be a blank instance. Great. Okay. Alright. So now I'm gonna log into this blank Directus instance. Directus is gonna give me all the back end functionality that I want and or need. And you could see it is totally blank. Right? So let's attack this from the back end perspective first, and then we'll switch to the front end, and then we'll switch to the back end, and we'll switch back and forth and back and forth until we all get confused with how many tabs I have open. Alright. Create a new collection. So let's create a new collection for our organizations. Right? As far as table names go, I prefer plural. Some people prefer singular. Totally up to you. We're just gonna add some of these specific default fields that Directus makes available to us. Like, hey. Is this published? Is there a sort order to this? Obviously, date created here will be populated with a timestamp of when this record gets created. Great. So now we have an organization. I'm gonna make this look very pretty. We will call it we'll search for maybe business. Forget I'm talking to myself here. I was about to ask you a question of which icon to use, but, still just talking to myself. Alright. So as far as our first field, right, we're going to add a the name of the organization. Pretty simple. Let's keep editing. I can make this shorter. We'll just go half width. So I am controlling the field, like the the actual form that we'll be interacting with as well as my data model and my APIs. So next, we will save this and let's add a website or URL. Let's just call it website. And what are we gonna do here? Let's go to advanced field creation. We could see that this is allowing a null value, but let's make sure that we set that to required. So when I fill out this form, it is going to make me input that. Now if I keep this as nullable, that means we could still potentially pass that value into the database as null, using the API. I could turn that off if I wanted to. I could also make sure this is unique as well, so that we don't generate this a million times. And for the display, we'll just show the raw value. Right? Is that it? Let's add a little icon just to make this a little fancy. This will be a link. Great. And now we have a website. And let's go ahead and make name required as well. We're gonna fill both of those things out. Boom. Amazing. I can go in. I can now add a name like Directus and a website, Directus.io. Save. Great. Notice that perfect little icon right there. It looks great. Right? I can even oh, no. I was gonna say I could point to it. I can point to it. There it is. Alright. So now we have this, like, data model. It's amazing. There's two fields here. This is really underwhelming as Bryant. Alright. So we've got an organization in our back end. Let's switch round to the front end, and let's start, like, fleshing something out. And this is where I will say, like, POC generation, AI has got an incredibly great for this, especially with, like, UI libraries like Shad CN and Tailwind for styling. It's gotten really good at creating components and things like that. So, what are we going to what are we gonna generate here? Like, what are we pitching? What are we what are we selling? Coffee mugs. Let's do something super developy developer ish. Maybe like a a seat cushion. Maybe we're gonna sell seat cushions. I'm hopefully, I look younger than I feel, but I've got three little girls. I've got a back that aches every day when I wake up. So maybe that's what we'll do here. We're gonna generate some seat cushions. Alright. So, when I think of the front end here, let's create a page. So let's create a new route for the org. Organization. I'm gonna wrap this in brackets there. Great. We'll do v base setup, script setup. Let's change that to length. Yes. And this is giant text, so that's a good thing. Alright. We got an organization. We're gonna fetch that organizational data. And behind the scenes, I've used this in so many videos. I'd never take the time to explain it. Basically, I'm using the Directus SDK. I've just got a quick little plug in as far as Nuxt goes. We're gonna pull the direct as URL from the configuration. We're gonna create a really simple direct as client, and we're gonna provide that to the Nuxt application. So the first time this application spins up or, you know, when it loads, it creates that instance for us and makes it globally available through something like this where we could say, Directus use Nuxt app. The other thing that I'm gonna do here, I'm gonna import read items from the Directus SDK and then we're gonna fetch this data. So we'll say data, that's gonna be organization. No thank you AI. Wait. We're gonna use the async data composable from Nuxt, just a handy little helper for caching. We're gonna call this organization organization. We're gonna get that organization from our routes, route.params.organization. No. We don't want that. We're going to do it this way, route.params.organization. We're gonna do direct us dot request. Yes. Direct us request. Read items, and we're just gonna wrap that. And do I have some nice formatting? Great. We got some formatting. Okay. So what are we doing here? We're looking for a slug. We don't really have a slug set up yet. So let's just do an ID. And maybe we do wanna set up a slug, maybe we don't. Route dot params ID. You'll notice this filter syntax here. Directus has a ton of different filter rules that I can use to create deeply nested or crazy queries where I can fetch everything I need in a single API call. And then I'm just gonna go in and log this out on the page. Is this possible? Let's go into Directus and just make this process easy for me as well. There is a button links option here which is just presentational. And we're gonna say view on website. Give it a label, arrow up link. Do it is there like an external link? Arrow. Let's just go arrow caret ray. Okay. And we're gonna say HTTP, we want the URL local host 3,000 is what we're running on. And then we're gonna run this at the ID. Great. Well, it's safe. Now I should get a button on this page where I can view on the website. And lo and behold, it looks amazing. This is the most beautiful page ever. We're just gonna go back in and edit our permissions. So by default, Directus keeps everything secure, keeps a tight lid on everything. So this public policy that we have doesn't have any access to our data. So let's go in and add read access for organizations and let's let's set files as well in case we decide to upload some pictures of our amazing seat cushions that we're going to sell. And now if I refresh the page, we could see, great, I have a name, I have a website. Alright. That's kinda underwhelming. Not great. So let's have it design. Let's retray, here. We are going to create I'm gonna use Claude's sonnet, and let's create a landing page. You are let's let's back up a minute. What do we want our landing page to look like actually? Right? So we probably got like a hero. We've got a features for our seat cushions, seat cushions, hero section. And then we've got like a like a buy button. Buy button. Buy now. CTA. Alright. So that's gonna be the structure for our page. Here we go. We are going to flesh this out. You know, we probably want to you are are we gonna, like, one shot this? Let's see. Well, if I do this, you are a expert I I don't think you really need to do this because it's picking up up whatever I have, but create a hero section component for a online store that sells seat cushions to developers. A beautiful, well designed view three component using alright. So one of the features I've been experimenting with in cursor is this documentation feature. This starter has like the Nuxt UI library built into it. Which version am I actually running? Did I upgrade this to the latest one? Yeah. I'm running the alpha version. So we're gonna use those docs and tell when CSS. We'll see what we come up with here. Let's create a beautiful new component. You page hero. I don't know I don't know that I have any of these actually. Just you don't use our Nuxt UI components after all. Okay. So we can see what is coming up with hero section. We got a feature, ergonomic design. Yeah. I'm kinda curious to see what it comes up with as far as, like, the data model around this as well. We'll just go ahead and flush this out for it here while component section, features. Oh, this is running kinda slow. New collection, blah blah blah, right column. Yeah. It would probably even be easier just to go into, like, Tailwind UI and find some of these components here. They've got some nice ecommerce components. Let's look at it like a hero section. Alright. Let's just we'll just see what it came up with first and accept this. We'll go back to our organization. We'll insert this component in here. This is our hero section. Great. Cushion. JPEG. Is there a cushion dot JPEG? Hero section. Where is this cushion dot jpeg cushion hero dot jpeg. Alright. Let's bring in one of our first AI big guns here. We'll go to replicate, and I'm just gonna use our flux model here. It will create a product image of a futuristic seat cushion for developers if I could actually spell, that'd be dangerous. Developer's seat cushion, developer's desk chair. I don't think punctuation is hugely important here. Let's see what we come up with. For 6¢ an image. What are we gonna get out of this? That looks pretty pretty wicked awesome there. Right? So we're gonna download that. Let's dump this into our assets folder, and we'll call it seat cushion dot JPEG, and we'll just update that here. Alright. So this should be assets, images slash seat cushion. And oh, what have we got here? Something weird is going on. Better comfort for better code. There we go. Alright. Now we're getting somewhere, except this looks really odd with our price badge. Something's going on there. Let's just comment that out. There we go. We got our seat cushion. This is great. Alright. Now let's have it designed something else. Right? Let me just shrink this down just a little bit. Feature alright. So we want this to be our headline. Right? So let's flush this out a little bit more. This is gonna be our headline. This will be our description. I'm just trying to think through like our what our props are gonna look like for this. Define props, features as a feature. Great. Learn more, shop now. Better comfort for better code. This is a new collection. Badge. Alright. So we'll just stick that in our props. Now we got badge, got features. Do we actually want the icons we can. I think I've got the Nux icon library built into this so we could simplify this, like hero icons. There we go. I don't know what other hero cons we have here. Shopping bag, check circle. There we go. And then we'll, like, flesh this out. Where is that icon at? Icon. Feature dot icon. Yeah. So we could ditch all of this and make this a little bit cleaner. We'll keep that. Right? You icon. Name equals feature dot icon. We'll just ditch this, ditch that, clean this up a bit, and we should still have our icons. Great. And then we're gonna move out features out of here, we'll put that into this. So for now, we'll just say here's our data, our data. We'll drop our features here. Sometimes it is frustrating as I'll get out to properly just navigate using these AI tools. The tab tab tab is it's nice when it works well. When you have all these extensions included, it does get a little bit messy. Let's check on time. Where is Figma? Thirty seven minutes. Alright. Better get to it, g. Alright. If I hit refresh, are we actually going to let's call this data, and then we'll go back into our hero section. We'll just wrap all this up into data. There we go. There we go. Data dot badge, data dot headline, shop now, learn more, data dot features. Okay. So maybe that is a little long as far as a headline. What do we have previously? Yeah. Alright. Comfort for better code. There we go. Okay. Now, this is our data. Right? This is our data structure. Maybe we tell this to generate. Alright. So how are we gonna generate the actual code here? Like, how are we gonna generate the personalization of this specific page? Let's just ditch the code part. There's our website. Looks great. How are we gonna personalize this? So for that, let's kick off a Directus extension, basically. Basically. Yeah. Nothing basic about it. So what I'm thinking we'll do here, we'll set up a custom Directus extension so that whenever I am adding a new organization, it will create that data for us. So let's also do this where we'll go in and we'll call this just page data. Page data. Page content. It doesn't really matter what we call it. I'm just gonna set it to be JSON. That's what we're gonna ask back from the API. Alright. Let's kick off this extension. So I'm inside my Directus directory. We're probably gonna go into CD extensions within that. And then I'm gonna go n p x create direct us or create dash direct us dash extension latest. And I am going to create a let's do a custom hook. This is generate personalized page, custom landing page, AI landing page. We use TypeScripts. We'll go ahead and install the dependencies. And one of the things that I've got set up here inside my Docker Compose for this, obviously, like, I'm I'm I'm creating a volume for the extensions here so we could pull those in. But I'm also somewhere in here. I have extensions auto reload set so that whenever we build this extension, Directus is going to look for that and reload this extension or at least it should. So we'll go into CD AI landing page, npm dev. Alright. So let's actually open this bad boy up and see what we've got here. Items dot create. Okay. So now I'm just quickly going to cheat a bit and pull up the direct to stocks. Hooks, custom API hooks. So what we're looking for here is after this item is created, and I think the structure for a collection itself is a collection name dot items dot create. This is what we're gonna do. So our filter will look something like the or our action here will look something like this. We don't necessarily need this. Alright. So we're gonna say organizations dot items dot create, organization created, and this will be async. And what this is going to receive should be alright. We're gonna get the context here. We can see what that context is gonna look like. That's how we're gonna initiate some services. What does the action receive? Right? The action receives the event name and a callback function that is executed whenever the thing is submitted. Alright. Extensions guys. Do we have one on hooks? Use hooks to validate phone numbers. Items that create input collection. So we're gonna get the payload, I'm assuming, and the content, item context. Let's see just kinda what we get here. Extension's changed. Okay. Test httpstest.co. If I check my Docker logs, I cannot import. Let's just stop and spin up the Docker container one more time. Make sure we're actually getting that. Okay. I could see that extension being initialized. Oh my gosh. That gets ugly, doesn't it? Alright. Let's try this one more time. We'll delete testco, test Co. Are we seeing any console log? I'm not seeing any logs here. Async, define hook. Extensions reloaded. Organization oh, duh. You gotta use the right right syntax, Brian. Frustrating to be under the clock. Testtest.co. Hit save. Okay. So now we're seeing some stuff here. Great. Alright. There's our holy moly. Alright. So that is our there's the context that we're getting there. Organization created. There's the payload that we're receiving. And then we're getting the item context. Okay. Great. Alright. So what we're gonna do, what do we want to actually happen when this is going to pop off? There's a third party tool that we're gonna use for this called FireCrawl that I've been testing. Right? So let's try this. We're going to add Firecrawl. Js. Alright. So we're gonna go to our extension that we're running. I'm gonna install this library from FireCrawl. And we're gonna go ahead and one of the other things that I'm gonna do, we're gonna call this the organization's service. We're going to extract that out of the context. Const request. No. It's gonna be the services out of the context, then the item service through the services. And we're gonna call new item service. Great. And this is gonna give us the ability to update. Right? So we're gonna initialize that service. We've got that installed. Now let's copy our code here. I'm hoping this is my live API. New FireCrawl app. We need to import that. Scrape a single web page. Got it. We'll move that down below our item service, and we're gonna get the scrape result. And for now, let's just oh, I can already see the typo happening here. That's gonna be frustrating for me. And we're gonna say await updated org equals await organizationservice.update.one. Data, does that need to be wrapped in data? I don't know. Scrape content. Alright. We're gonna return the updated org, and let's wrap this in a try catch as well. Try catch. Console dot error. See if we get some formatting. Nice. Okay. Let's fire up the dev server for this and cross our fingers. Right? The other thing I need to update here, we're gonna have to go in and figure out which tab has all of my browsers in it. Maybe we just move that to a single browser for now. And we're gonna add a new field for this. We're gonna call it, scraped result. Scraped result. I don't know what this is gonna be. I'm assuming scraped content. I'm assuming this is gonna be JSON data that we're gonna get back. I guess I could look and see. Success data. You know, we could also just store the markdown. That might be easier. Where do we go? Scrape content. Let's delete that. Where does this store the markdown? Scraped. Scraped content. Alright. So according to FireCrawl, if we do this correctly, we're gonna get something like this. So with the scrape result dot data dot markdown. Fingers crossed. That's what we're gonna get. We could also just console log that to verify scrape content, scrape result. Alright. Let's go for it and see one last thing that I wanna do here. This is not, using, like, any of our accountability, so do not do this in production. But we'll just allow the public to create and update all the content on the underlying instance. Great. Love how secure I am being. I'm sure everybody on my team would be thrilled. Alright. So let's just go to Directus as the example. HTTPS, Directus.io. That's our website. Oh, I've already got directis.io hard coded in there as well, so we're probably gonna want to pick that up out of the payload as well. Can we still see that payload? No. Let's do this. We'll see the payload.website URL. We'll see if this actually works. Fingers crossed that it does. Scrape content. Save. What happens on this side of it? Around URL must be a top level domain or valid path. So I'm getting that error there. It's fine. Should set this up on update or create. Directus.io. Save and stay. Do we see what we get back? Is this thing still running in the background? What are we doing here? I got no visibility into what's happening here. Let's try it again. Directus. Directus. Io. Hit save. Must be a failed to scrape URL. URL must oh, duh, dummy. This is where it helps to define types so you don't make these same silly mistakes as I have. So I've called this field website. I was calling it website URL here, so not actually passing any website. Cannot read properties of collections. So there's our scrape results. Do we see the markdown? There's our markdown. Cannot update one. What am I doing wrong here? We're not updating the schema. We need to extension service, access items. Yes. Here we go. This is what I actually need to see. Alright. So we got our context. We're gonna do services get schema. We need that schema. And we're also gonna need the accountability schema. So in our item service definition, we don't wanna do this. We're gonna do this. Equals await. No AI. No. We're gonna await git schema. So we're gonna get the schema from our context and on the accountability side, accountability. That'll be accountability. If we just destructure that, I think that gives us what we need. Let's go back and try it again. Love hacking this stuff together on the fly. No pressure. Directus directus dot I o. Hit save and stay. Let's just watch over here and see what happens. Fire crawl seems to take a little bit, so that is forbidden. I don't know what we're getting over here. Can I just leave off the accountability object? How many times am I gonna create this before I just do update? Directus Directus dot I o. Save. Probably burning through these fire crawl credits as we speak as well. That that that that that, forbidden. You don't have permission to access this. Why is that not the case? What is the alright. So let's stop loading that. Let's just say console dot log the payload. Actually, I think it's I goofed up, didn't I? What are we gonna be receiving here? It's not gonna be it'll be payload dot key. Payload dot let's just log the whole payload again and see. Just comment out this. Comment out. Directus Directus dot I o slash payload key. Okay. Duh. You're smarter than that, Brian. Alright. So we need this key. That's what we're getting is the key payload dot key. So here, payload dot key. There's the event. There's the key. There's the organization of the collection we already got. Alright. Let's keep rolling and maybe try to actually complete this one. Directus. Directus dot I o. Save. Can we actually get the scrape content? Can we actually complete this on time? Can we do any of these things in any sort of actual working fashion? Is this actually gonna work? Will we know? How do I know? Console.logupdatedorg. Directus. Directus I o. Save. There's the payload. Fire crawl really takes a bit. Status code, 200. Updated org. Is this actually gonna see what we want? No. Of course not. It's not gonna update the right one. Data dot scraped content. I'm sure this is what we need to do here. Right? Update one. Let's look at the services. Update an item. Yep. It's not inside data. So I don't need that to wrap. Gosh. I'm just eating up all the time. I wish I spent more time with hook extensions. Super powerful because you're basically just updating the underlying Express app instance anyway. But alright. So now let's reach for anthropic. If we hit refresh, do we ah, still not getting the data that I want. Right? It's saying that it's updated that org. Scrape result. Scrape result scrape content. So they tricked me. It's not it's not as it shows. It's actually not there's actually no data attribute there. It's basically just scrape result dot markdown. Alright. Last freaking time for the scraped content, Directus Directus dot I o. I was not planning on doing this much troubleshooting. Behind the scenes here, I'm gonna go in and just create a API key for anthropic. Hundred apps. There's my API key. Updated org. Did we actually get what we want? Holy moly. We got some scraped content in here. Okay. Alright. So now the next step in the puzzle is going to be call the Anthropic API to generate a landing page for the organization with the JSON schema, page data. Alright. So now this is one of the things where I've been leaning on the Vercel API, or AI SDK. So I'm just gonna install it here, p m p I. This is just AI. And they've got a way. The last time I checked, the Anthropic does really well. Like, SONNET does really well at copywriting. But it doesn't have the JSON mode that the ones like OpenAI have. So I'm just gonna go in and look for provider management providers. Where are my providers? Providers and models. Looks for the anthropic provider. I'm gonna add it as well. And let's see what we can get here. Import anthropic. So I'm gonna import this guy. And then we're also going to do what? Create anthropic. We'll do that here. Now what I would normally do is this context object should have an e and v variables as well. API key. I'm not gonna do that here because I am worthless, basically. No. We're eating up a lot of time. Base URL, API key, generate text. We want to generate objects. So there's our anthropic client that we're creating. Don't mind that API key. Now let's define what do we need? We need Zod as well, p m Zod. And we're going to use was it generate object in here? See if we can find it. Generate object. Alright. Generate object from AI. Okay? Now what we're trying to do is just basically, we're gonna define a Zod schema of the object that we want, and that's gonna be our hero section. Right? Zod schema. We're gonna lean on the props. There we go. Okay. So now I'm just gonna copy this wholesale. We're gonna stick this in our custom hook. Again, not being careful at all about separation of concerns or cleanup. So there's our Zod schema. We want the object to return hero section schema. Alright. So now let's look up what we actually want this to how we're gonna work this. Generate object. We don't want testing. There's gotta be a better during text, during structured data. Alright. So our model okay. So we want the object equals await generate object. I'm hoping this is right. Fingers crossed with AI. Generate a landing page for the following organization with the content. Okay. For the organization payload. Name, the following content. You are an expert copywriter, and your job is to generate amazing landing pages that are personalized for our target clients. We sell the best desk chair cushions in the world for developers. Schema. There's our schema. That's our Zod model. Okay. When we get that back, what are we gonna do with the object? We are going to update the org, updated org two. It's fancy, fancy, fancy. Okay. Alright. How we doing on time? We got nine minutes to try and figure this out. Let's just PM, PM, dev this bad boy up and see if this is actually gonna come out at all. We're rebuilding this extension. Dun dun dun dun dun. I'm gonna nuke this entirely. Let's try it again. Let's see if it works for directus.io. We hit save. I could check the logs on Docker. Create anthropic is not defined. Anthropic. Response. Anthropic. Where is that? Providers and models. Anthropic provider. Create anthropic. Import the default provider. Oh, no. We could just import the what am I doing? Create create anthropic. Organizations. Oh, no. Directus. Duh. Directus .I o. Hit save. There's the payload. Obviously, it would be great to have visibility into this. And one tool that I've been using as of late is ingest. Great tool for something like this, generated hero section. This is gonna be nice, doc. Okay. Alright. So now before we reveal what it generated, let's just try to make sure this is actually gonna work. So instead of the data here, we're just gonna use the organization dot data dot page data. Organization dot page data. Generated hero section. That looks nice. Organization dot page data. Dun dun dun, the big reveal. Close that guy out. No. Status message. What is the status message? Did we actually get the page data generated? Okay. We got the page data generated. It looks like it's coming back with data here as well. So we should probably fix that in our hook. Page data page data is what? Object dot data. But for now, what I'm gonna do, I'm gonna just strip this out. Right? Great. And we refresh. We refresh. We refresh. No. What is going on? Organization. I'm excited for this, and I'm hoping we could get this to work. Right? Why am I not getting any of my data? Organizations. Access policies. We can see that organize oh, that's because it's not the right organization. Alright. I was using the old organization. We blew that one away like a hundred times. Right? This is probably where you'd wanna slug. Five minutes left. What do we have on the clock, Johnny? No. It's not working. Boy. Alright. There's our page data, organization dot page data. There's the scrape content. Why are we not seeing that? Right? Organization. Let's try it now. Status code. What is the error that we're receiving? V if organization dot page data. I could see the page data, man. Organization. Oh, duh. Gosh, man. What an idiot. Alright. So I I've done this on another episode as well. We're gonna transform this to get data. Where is this at? Is it here? Transform data. I don't know if this is how it functions. Error results, organization. I'm not sure the exact syntax, and I'm not gonna mess around with it. So we're gonna do this. It's gonna be organization. Boom. Bam. Directus plus Comfy Desk revolutionize your development experience, Seamless integration. Integrate Directus' flexible back end with your favorite front end while sitting comfortably on comfy desk cushions. Tailor your with adjustable cushions. Perfect. Alright. So now we have landing pages that are personalized for a specific client within, like an AI workflow. Right? So let's try this with some other company. Let's say OpenAI. I guess just to to pit these guys against each other here is this OpenAI.com. OpenAI Com. We'll hit save. And hopefully, this all runs, works nicely on the back end. There's the scrape result. You know, if I wanted to get really fancy, obviously, I could take this to the extreme here. But let's look at OpenAI. We'll view this on the website. What is going on? For noose proper next string, OpenAI. Okay. This one is not super strong. Right? Not sure what the scrape data result was. It looks like Firecrawl didn't do a great job with their website. Right? Let's try Apple.apple.com. Save. And we'll see what it comes back with. There's the scrape content. And, again, this is just coming back with markdown. You know, I might have better luck with, like, actual JavaScript parsing or something like that. But boom. Elevate your comfort coding experience with Apple inspired comfort, the iCushion Pro. Anyway, that's it for this episode. We've achieved this. I'm pushing a stop button on it. One minute left, we have basically fleshed out this personalized landing page where we add an organization, we use AI to scrape it, we then we generate content using LLMs, based on a specific schema, and then boom, we've got a personalized landing page that we could share with them. As far as next steps for me would probably be, like, setting this up on, like, a pretty URL or a slug, you know, adding this to some type of outbound, like, outreach campaign potentially. That's it though. It's amazing what you can put together in just an hour with tools like Cursor, Directus, Nuxt, Anthropic, all these different things come together to create stuff that's fun to build. I'll catch you on the next episode. Peace.","e7c271ec-4424-48b6-9063-d3afa4ac3fe4",[677],"6f802ad1-c2aa-47f4-8770-e73bc9a5fc41",[],{"id":142,"number":143,"show":122,"year":144,"episodes":680},[146,147,148,149,150,151,152,153,154,155],{"id":155,"slug":682,"vimeo_id":683,"description":684,"tile":685,"length":524,"resources":8,"people":8,"episode_number":358,"published":556,"title":686,"video_transcript_html":687,"video_transcript_text":688,"content":8,"seo":689,"status":130,"episode_people":690,"recommendations":692,"season":693},"ai-copilot","1059438523","Bryant takes on his most ambitious challenge yet: building an AI Copilot that integrates directly with Directus data. Watch as he races against the clock to create a custom chat interface that can query your database, implement tool calling functionality, and (attempt to) persist conversations. Things get meta when Bryant uses AI to help build his AI assistant—with some hilariously chaotic results along the way.","b22143bc-51f9-4460-9c1e-45b2b9735375","Mission: AI Copilot","\u003Cp>Speaker 0: Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie, for Directus. And today is a very ambitious hour. I've got no clue how this is actually gonna go, but we are going to try and build an AI copilot that sits right alongside our data, inside our back end, which is Directus. Alright.\u003C/p>\n\u003Cp>If you're new to the show, 100 apps, one hundred hours, we have sixty minutes to plan and build an application, a clone of Airbnb, did some something we can actually ship and pat ourselves on the back at the end of the day. Or, you know, more often times than not, we struggle and then we, fail publicly. You know, sometimes that stinks, but it's all in good fun. The second rule of 100 apps, one hundred hours is use whatever you have at your disposal. So, AI tools, past projects, you know, if I my kids could code at this point, I'd probably leverage them.\u003C/p>\n\u003Cp>But, it should be a fun episode today. Let's dive in and try and figure out what we're actually doing. Let's put sixty minutes on the clock. Boom. AI Copilot go.\u003C/p>\n\u003Cp>So, at this stage of the game, AI is very impossible to ignore, and, you know, chatbots are everywhere, very commonplace. You know, lots of folks using Claude, chat GPT on a daily basis. Even my kids are aware of what it is. So, what we're gonna do is try to build an AI Copilot into our Directus instance. So I've got this blank Directus instance.\u003C/p>\n\u003Cp>I'm imagining this as a module. Directus is super flexible as a back end in the CMS because we got full control over what's going on. I'm imagining it as a a module with, like, a standard chat interface. I don't know if we'll get there or not, but let's just discuss functionality. Right?\u003C/p>\n\u003Cp>We want to be able to chat with AI models, specifically LLM models. Cat. You have to actually learn to spell Bryant. Alright. We wanna be able to chat with the LLMs.\u003C/p>\n\u003Cp>We want to be able to use tool calling. I think that's the technical term to fetch Directus data. Data stored in Directus. Data stored in Directus. We wanna do this do all this through a beautiful chat interface.\u003C/p>\n\u003Cp>Alright. That's our functionality. Right? We're gonna probably have as far as our data model, we'll have, like, a conversations to, like, persist these. We've also got our data, which I'm going to leverage some of our existing, like, starter templates.\u003C/p>\n\u003Cp>Great. Okay. So there's the arrows drawn in the sand. Let's let's dive into this thing. The first thing I'm gonna do, I'm gonna load up some sample data.\u003C/p>\n\u003Cp>So what I wanna do is go in here and create a token. I could just use my standard password as well, but we're just gonna copy that out. This is set up at local host 8055. I'm gonna come into the terminal, open up a new terminal instance here, and once that spins up, I'm gonna do directus m p x. I guess I can make this big so we can actually see it.\u003C/p>\n\u003Cp>Right? Directus template CLI at latest. Apply is the command I'm gonna run, and this should load up some templates for me or the ability to load a template. Fetching. Okay.\u003C/p>\n\u003Cp>So we're gonna apply a community template. Looks great. Let's do let's just do our simple CRM for this one. You know, I could the simple website CMS, super excited about that template. I love that one.\u003C/p>\n\u003Cp>For, like, a assistant, maybe CRM makes more sense. So we're defaulting to eight zero five five. I'm just gonna add my token in there, and you could probably steal this token. This instance is gonna be destroyed, and I'm not sure I'm I'm sure somebody could figure out how to get access to my local host. I know we got a lot of really great hackers out there in the community.\u003C/p>\n\u003Cp>That template was applied successfully. And now I'm just gonna hit reload, and the first thing I see is Michael Scott there. Alright. So now I've got some sample data to at least work with. We got some organizations.\u003C/p>\n\u003Cp>Great. So let's dive into how we're actually gonna pull this off. If I map this out, basically, what we're gonna take advantage of are extensions in Directus. Extensions, extensions, extensions. Two types specifically.\u003C/p>\n\u003Cp>Right? So for secure communications to our LLMs, we want to have a API proxy endpoint. So we're gonna set up an endpoint, and I'm pretty confident we can get this done. The actual chat UI, I'm not sure that will be what we call a module. So a module is basically just whatever functionality that you want.\u003C/p>\n\u003Cp>We give you tools like composables to access data inside the Directus instance, and then we give you free reign. So, like, a good example of a module is our command bar or command pallet. This is an extension that you can install from the marketplace. What it does is it adds a command k search to every bit of Directus so I can quickly search no matter where I'm at, whether I'm on contacts and I wanna search organizations, blah blah blah. It also comes with, like, a a settings page, and it's MIT licensed.\u003C/p>\n\u003Cp>So, you know, if you are looking at building, like, a custom module, this is a good one to copy because it has a ton of functionality and, it had a small hand in it. Kudos to Hannes from our team for, dude, just making this freaking wicked awesome one. Alright, but back to the task at hand, we're going to create a custom endpoint for this. So locally, I'm not sure if you can see this or not, we've got our databases here and then we've just got this Extensions folder. There's the Registry, which is when you install extensions from the marketplace.\u003C/p>\n\u003Cp>We're just going to create a new extension for this. So I'm going to open up this inside the terminal somewhere here, get this back to a reasonable level where I can actually develop, and I'm gonna do npx create create directus extensionlatest. Just look at the screen, I'm terrible at narrating. We'll go ahead and install this. It's been a minute since I've done this.\u003C/p>\n\u003Cp>And we offer another type of extension where it is, which is called a bundle. So a bundle is basically wrapper, like we're building a ChatGPT wrapper. A bundle is essentially a wrapper where we can, like, bundle some extensions together. That's a bundle. Right, we can distribute an endpoint and a module within a bundle, that's what we're gonna do here.\u003C/p>\n\u003Cp>So I'm gonna scroll all the way down, we're gonna do bundle, we're gonna call this Directus, let's just call it AI Copilot, that's what we'll call it. Yes. We wanna auto install dependencies. And now inside our extensions folder, you can see Directus is fleshing this out for us. We've got the Directus Extensions SDK, and then we can see there's nothing else in here.\u003C/p>\n\u003Cp>Right? So the next thing that we wanna do is CD Copilot, and we're going to hit npm run add so that we can add an extension to this bundle. Yeah. The if you're not using a bundle, like, it should automatically initiate the extension type for you. But in this case, we want to do an endpoint.\u003C/p>\n\u003Cp>We're going to call this Chat Endpoint. Let's use TypeScript and this will create a new Directus endpoint for us. Very simple, You'll notice a Request Response here behind the scenes, just an Express app. And next, we're going to talk about what we're actually going to use. Giving myself a seizure here, switching back and forth.\u003C/p>\n\u003Cp>So, I've been experimenting in other projects with the AI SDK from Vercel. That's what we're gonna use here and try to get as far as we can and try to make this really nice in a short period of time. So let's fire this up, right? Docs. What do we need to install first?\u003C/p>\n\u003Cp>Right? Do I want to use OpenAI or Anthropic? I've got I think I've got an Anthropic API key locally already. Do I do I not do, EMP? Yes.\u003C/p>\n\u003Cp>I do have a API key locally. Great. So let's do that. Right? We are going to do an overview.\u003C/p>\n\u003Cp>We want to install PMPMIAI. We're gonna use Anthropic as a provider, AI SDK Anthropic. And because we're using Express here, we're gonna wanna import Express as well, or install Express. We're probably also going to wanna have Zod. Right, if we get into, like, the tool calling, Zod is a big one, so we'll add Zod.\u003C/p>\n\u003Cp>That should give us the tools that we need to work on this, and let's just look at our custom endpoint now, right. The next thing I'm going to do is hit pnpm dev inside this. This will set up a watcher for our extension, and I'm just going to stop our container and restart just so it picks up that extension now that we've built it. And what this is gonna do, it should now be available at slash chat endpoint. Let's see.\u003C/p>\n\u003Cp>Local host. So if I refresh, we should see that extension. There's our chat endpoint. Let's hunt down the URL for this. Chat endpoint.\u003C/p>\n\u003Cp>Hello, world. There it is. Boom. Boom. Boom.\u003C/p>\n\u003Cp>Great. So how can we control that if we want? Set up some of this documentation here. Alright. So the the next thing that we're going to want to specify, I could specify, like, an endpoint or an ID for this when we define the endpoint.\u003C/p>\n\u003Cp>Right? If I put these up side by side, export defaults. We're gonna call this the chat. Chat. We're gonna have a handler for this.\u003C/p>\n\u003Cp>And in the handler, we'll provide the router and the context. Alright. Now we save. This should reload because I've got that set. And now if we load this again, chat endpoint doesn't exist, but now chat will.\u003C/p>\n\u003Cp>Right? Great. So now we can control what we want that endpoint to show up as. Awesome. Alright.\u003C/p>\n\u003Cp>What's next? Right? We're going to actually pull this in and set this up. Alright, so a couple of things that we're going to do that you probably noticed here, right? If we want to pull the API key from our environment, we're gonna want to actually pull that out, right.\u003C/p>\n\u003Cp>So let's do Destructure This. We're also gonna do we actually need that? No. Let's just keep it simple for now. Let's look at this AI SDK, and we're gonna import createanthropic, and we're going to create an anthropic client.\u003C/p>\n\u003Cp>Create an anthropic API key. We're gonna leave the base URL the same. Great. I'm not sure why we're duplicating that. Right.\u003C/p>\n\u003Cp>We're gonna add a router. This is gonna be a post method. So let's now take a look at was it did I have a I have an express option in here? Okay. So here's what a typical express setup might look like.\u003C/p>\n\u003Cp>So we're gonna import z from za. We'll go ahead and import za. Let's also do stream text from AI. We're gonna use router dot post, and let's see what AI comes up with. Message request dot body stream twenty twenty.\u003C/p>\n\u003Cp>Let's see the newer model there. Provider instance, anthropic provider. Does this list out their models? Okay. Yeah.\u003C/p>\n\u003Cp>Here's the latest model for Claude SONNET. That's what we're gonna use in this case. And we got messages, role, user content, message. We're just gonna send the stream. What is the is there a helper inside here for this?\u003C/p>\n\u003Cp>Result dot stream. Results dot pipe stream to response. Let's call this the result. Alright. And let's see if this is actually going to give us what we want.\u003C/p>\n\u003Cp>Alright. So we post a message. This should send something to the AI. I'm just gonna go into hundred apps. We're gonna create a new request.\u003C/p>\n\u003Cp>We'll say HTTP local host eight zero five five slash chat. Oh, that's the name and not the actual URL. Chat. Alright. We got a body.\u003C/p>\n\u003Cp>We're gonna create a JSON body. It's gonna have a message. What is two plus two? Send HTTP request wrong version number or prototype blah blah blah blah blah blah. Maybe we should wrap this in a try catch, see what we got.\u003C/p>\n\u003Cp>Oh, clean that up for me. Oh, well, no. No. No. No.\u003C/p>\n\u003Cp>No. Try catch console .log dot error. Do we have formatting? I do have some formatting enabled in this. Let's just try to send the results.\u003C/p>\n\u003Cp>Pres that's in result. Extension's reloaded. What is this showing? StreamText. Create anthropic API key.\u003C/p>\n\u003Cp>Console dot log. Do we have the actual ENV? We do have the ENV. Did I name it the correct thing? Oh, god.\u003C/p>\n\u003Cp>What an idiot. HTTPS should be HTTP base stream, send result. I don't know that the actual Bruno here will, like, handle streaming. What if we just tried, like, generate text? Generate text.\u003C/p>\n\u003Cp>Awaits. Let's just make sure this is gonna actually work first before we get crazy with it. Generate text results dot send. Result. Alright.\u003C/p>\n\u003Cp>Let's pull chat over here. What's two plus two? We hit send. Okay. What is two plus two?\u003C/p>\n\u003Cp>Finished reason, two plus two equals four. There's the text we get back. Okay. So now we've got, like, some type of AI proxy going on. Right?\u003C/p>\n\u003Cp>Let's jump to, like, tool calling at this point. We have, like, forty minutes left. You know, I read through the documentation for this. Not sure we're gonna get this correct, but alright. This may be where we just lean on AI to build AI and call it good.\u003C/p>\n\u003Cp>What I'm gonna do let's just copy all of this. Right? And this wouldn't be fun if we didn't, like, make it super meta and have AI create AI. Let's say something like this. Here's the documentation for tool calling with Vercel AI SDK in Node.\u003C/p>\n\u003Cp>Create a tool for fetching data from the Directus instance using the items service. Alright. So when it comes to, like, custom endpoints in Directus, one of the things that you do have access to is all the services that we use internally as in Directus, and, hopefully, this is gonna do a somewhat decent job of that. We'll kinda see what it comes up with. But if I take a look at, like, the extensions docs and we go to extension services, like, how do I access items?\u003C/p>\n\u003Cp>Right? Whenever I define a custom endpoint, we receive the, you know, router instance that we can plug into, and then we get this context. So the context has the EMV, has our services, it has this helper function for getting the schema. And then we define a new item service based on the collection that we're gonna call. Right?\u003C/p>\n\u003Cp>Let's see what we come up with. So I'm just gonna hit apply. We'll kinda go through this. We got stream text, generate text, item service, this looks okay. We're also gonna need schema, services get schema.\u003C/p>\n\u003Cp>We're gonna need that as well. Alright. So what is this actually doing? It's creating tools, multiple rounds, query from a a Directus collection. What's the filter?\u003C/p>\n\u003Cp>Object equals await get dot schema. Request dot accountability. Can we just leave that part off? That will be read by query, filter, success, return the items, the count, accept the file, await generate text. Something is off here.\u003C/p>\n\u003Cp>We got, like, one too many. Where are we going wrong? There we go. Okay. Alright.\u003C/p>\n\u003Cp>So, hopefully, if we invoke this tool, there's a description. Now let's try a new message, and let's just ask it let's pull up Directus. Right? Simple question. Right?\u003C/p>\n\u003Cp>Like, how many organizations do we have? How many organizations are in our Directus instance? There are six organizations in the Directus instance. These are Health Plus, Technovus, Solartec, Tesla. Boom.\u003C/p>\n\u003Cp>So nailed it spot on. We can see kind of what's going on here. There's the context that that we provided, I guess. I could see, like, the answers there. That's great.\u003C/p>\n\u003Cp>I'll help you. Okay. So there's the actual organizations. Cool. So now we've got kind of this tool calling functionality into this endpoint, which is pretty nice.\u003C/p>\n\u003Cp>Do we want to make it aware of the schema of Directus? I'm not sure, like, is this giving it enough information? Or tools, query collection. What does our actual schema look like? Right?\u003C/p>\n\u003Cp>Console log, await. Console schema equals await git schema. Oh, it's gonna be a async handler. Hansel dot log schema. We'll just stop logging that.\u003C/p>\n\u003Cp>Directus flows, schema, deals, activities, deal contacts. Yeah. So the other thing to note that this could get expensive pretty quickly. Alright. Okay.\u003C/p>\n\u003Cp>So now we are cooking at least with something. Right? Let's try to go in and focus on, like, this chat interface. Right? Now how are we going to do that?\u003C/p>\n\u003Cp>We're going to go back to our extension. I'm going to disable this for now. We're going to hit npm run add. We want to add a module for this. So this will give us a, we'll call this a Copilot module.\u003C/p>\n\u003Cp>Chat module, whatever. There we go. So this is gonna add another folder within our AI Copilot. So now we got the chat endpoint. We've got the Copilot module.\u003C/p>\n\u003Cp>And within this module, now if we just build this, we should see this module becoming available in our settings for the project. Custom. Custom module. There we go. So I can actually define this.\u003C/p>\n\u003Cp>Right? So each module or a lot of the interface extensions have this index dot ts. We're gonna call this our Copilot module. Let's call it Directus AI Copilot Magic. Is it gonna be Magic?\u003C/p>\n\u003Cp>Oh, there's not a Magic icon. What do we have as far as icons? Designer OCD gets me every time. A recovering designer. What is that one called?\u003C/p>\n\u003Cp>Magic button. Magic button. See if that gets us where we wanna be. Discard. Refresh.\u003C/p>\n\u003Cp>There we go. Alright. So I'm gonna enable this. We're gonna move it up to the very tippy top. There's our module.\u003C/p>\n\u003Cp>Here's our custom content. Boom. Didi boom. It's just a simple view component. Right?\u003C/p>\n\u003Cp>We can get as complex or as detailed as we want. On the client side here, we have access to some composables. Right? So on the back end, like the the Node. Js side, we've got, like, this item service we can call.\u003C/p>\n\u003Cp>On our other extensions where's the extension types? There we go. Like interfaces, for example, accessing internal systems, we can access, like, stores through this. We can also access, like, the used API or the SDK. Great.\u003C/p>\n\u003Cp>We'll just pull in the API. And now let's look at the documentation. So by no means am I an expert here, and this is probably where things are gonna get pretty hairy. Like, if we recap, we've got a proxy endpoint. We've implemented just a tool to fetch Directus data and include that in our Copilot.\u003C/p>\n\u003Cp>Now we need to actually build this, like, this chat UI that we're so used to seeing, right? And Directus is using Vue, which I love Vue. Vue is amazing. Most of these AI, like, pre built chat UIs are React based. So if you know of a good one that's for Vue, for building chatbots really quickly, let me know.\u003C/p>\n\u003Cp>So let's go here to SDK UI. Right? They do have support for, like, Vue JS. I see that here, this use chat function. We're gonna need that AI SDK view, so we might as well go ahead and install that as well.\u003C/p>\n\u003Cp>AI SDK view. Okay. There's the API endpoints, conversational interface. Is there, like, a simple recipe for this? Stream checks with a chat prompt.\u003C/p>\n\u003Cp>Alright. So, again, this might be a great thing to lean on AI for using here's an re a React example from our the Vercel, SDK. Or, actually, let's just piggyback off that last chat. Let's see. Here's a React example from for chat UI from Vercel AI SDK.\u003C/p>\n\u003Cp>Oh, actually, I have to show that. Build a view version of this that works with our chat endpoint. There we go. See what AI comes up with. So it's creating another composable.\u003C/p>\n\u003Cp>Why are we just use the built in used chat from Vue AI SDK. How do I go back to that? Chatbot AI SDK, chatbots. That was what? Under the API reference?\u003C/p>\n\u003Cp>Use chat view. And I just yeah. SDK view. Okay. And now we're getting somewhere.\u003C/p>\n\u003Cp>We got some messages. We got v button, v text area. No. Alright. Let's let's try and look at what's going on here.\u003C/p>\n\u003Cp>Handle submit, use chat, use API, AI Copilot, message content. So here's our messages. The v text area, v button, these are built in components within Directus. It is using Tailwind, which we we don't use Tailwind to Directus. So, use vanilla CSS instead, And then we can hit save and see if this is actually gonna work.\u003C/p>\n\u003Cp>Right? Now, I will tell you, like, I I've been using Cursor a lot recording this season of a hundred apps, hundred hours. I like it a lot, especially for prototyping. I found myself definitely suspect of the code sometimes, so don't just tap, tap, tap, accept. Yeah.\u003C/p>\n\u003Cp>Be very deliberate. And before you ship anything to production, make sure you test all this and go back and understand what's going on. This is not going to work, obviously. I just know our variable syntax, all of these have dash theme, dash dash in there. So hopefully, this solves that, but those are the things that you gotta watch and sometimes where, like, the AI Copilot stuff can get in the way.\u003C/p>\n\u003Cp>Right? How many orgs, organizations are in Directus? There is no icon button here. What would that be? Send.\u003C/p>\n\u003Cp>Google material symbols. Is there a send? There is a send. We don't have a icon prop here as well, so we're gonna do something like this, like v icon name equals send. That should give us the icon that we're looking for.\u003C/p>\n\u003Cp>Yeah. There we go. Okay. How many organizations are in Directus? Alright.\u003C/p>\n\u003Cp>So is this actually gonna work? Who knows? We'll see. Chat is not coming back. Internal server error.\u003C/p>\n\u003Cp>There's the messages. I don't think we're gonna need to pass the ID of this. Right? So if we look for a unique identifier for the chat, let's not use that. We got slash chat.\u003C/p>\n\u003Cp>Where is the local host slash chat? Alright. If we look at our endpoint, right, we need to adjust this. Right? We're gonna do StreamText, and we want to do the result for that.\u003C/p>\n\u003Cp>So we're gonna pipe that stream. Messages, content dot messages. What is the example for this look like? Is there where's that node? Like, so much of development for me is diving through documentation and just bouncing back and forth.\u003C/p>\n\u003Cp>Express. You can set up an Express server, stream, generate text, stream text with a chat prompt. Stream object, stream stream text. You probably just wanna look at it like the actual APIs here. Stream text.\u003C/p>\n\u003Cp>What are we passing to stream text? We've got messages that we're gonna pass. So those are just gonna be coming from the body. Right? Those are gonna be messages.\u003C/p>\n\u003Cp>We'll just pass those messages. And is that gonna get us what we want? Tools is already there. Do we need, like, a max tokens? No.\u003C/p>\n\u003Cp>Alright. Let's just see how far this gets us. How many organizations are in Directus? Okay. There's the so we're seeing the response.\u003C/p>\n\u003Cp>I can see the actual response there, but I'm not seeing it show up in our actual module here. Right? Messages. Alright. Use chat.\u003C/p>\n\u003Cp>Append. Let's just test what they're doing. Yeah. So that's kind of what we want there. Append content inputs.\u003C/p>\n\u003Cp>Result to data stream response, system helpful assistance, messages, I don't know why we have handle input change. Set appends. Let's see what's actually going on. This is where things get tricky. Right?\u003C/p>\n\u003Cp>Is there a handle input change inside the use chat model? Use chat. Initial message, initial input on. Handle submit. What does this return?\u003C/p>\n\u003Cp>Handle submit. I don't think there is a handle input change. Right? So, again, this is where, like, things get dangerous with AI, where you take 10 giant steps forward, and then you take, like, six giant steps backwards just trying to figure out what's actually happening here. So, chatbots.\u003C/p>\n\u003Cp>Where's the core StreamText. Well, actually, let's look at use chat on this side. Right? Initial input, initial messages on tool call, on response. If set to stream protocol, stream protocol should be text, I guess.\u003C/p>\n\u003Cp>Let's see how many orgs are in Directus. Oh, there we go. Okay. Now we're getting somewhere. Right?\u003C/p>\n\u003Cp>Okay. So now we're seeing the text being streamed in. If I wanted to select this, I can't because of what we've got set up in Directus. But, eighteen minutes in, like, we've got chat with AI models. We've got tool calling.\u003C/p>\n\u003Cp>We are somewhat through. Like, maybe we strike through half of that. We do all this through a chat interface. Alright. Now alright.\u003C/p>\n\u003Cp>Let's dive into, like, persistence. What do you think of those names? What do you think of those names? Okay. Yeah.\u003C/p>\n\u003Cp>We should probably have some type of, like, markdown formatting here as well. Is that what's coming back from the actual API? Is it just markdown? Yes. Okay.\u003C/p>\n\u003Cp>So do we have do we have a markdown helper inside Directus? Directus dash Directus issues. Cisco markdown. Directives markdown. Can we access this directive?\u003C/p>\n\u003Cp>The markdown. The markdown. Or we're buying that as MD? BMD? BMD.\u003C/p>\n\u003Cp>Okay. So can we do this? VMD equals message.content. Obviously, we're gonna lose that. Hey, o.\u003C/p>\n\u003Cp>Yeah. There we go. Now we're in some formatting. Looking nice. Looking nice.\u003C/p>\n\u003Cp>Okay. Get specific fields from a collection. Here to help you work with direct to collections. What do we have inside the database? Alright.\u003C/p>\n\u003Cp>Contacts. Who is the coolest contact in our CRM? Each of these contacts has their own interesting qualities. Without knowing your specific criteria for coolness, it is hard to definitively say who is the coolest. Edon kind of sounds like Elon.\u003C/p>\n\u003Cp>This might be a playful reference. Who knows? Who knows? Alright. So this is super interesting.\u003C/p>\n\u003Cp>Right? Let's work on persistence. Let's work on persistence to our directus database, what would the best data model be knowing the structure for messages. So if we look at stream stream object, stream text, And, like, our endpoint here is just streaming back the pipe text stream two response. Is there, like, a pipe data stream?\u003C/p>\n\u003Cp>Yeah. There you go. That's how we could get the actual messages. Pipe data stream to response. Send message data object.\u003C/p>\n\u003Cp>Okay. And then we should be able to like, inside our module, we can use the stream protocol. Let's just set this and see see if this is gonna work as well. Hey, o. Okay.\u003C/p>\n\u003Cp>But now instead, what we're getting back is actually the message data as well, which is probably what we're gonna want. Alright. What did you come up with, friend? So we've got messages, conversations, messages. So this is actually trying to give us a direct to schema, which kinda scares me a bit.\u003C/p>\n\u003Cp>So we got a user conversations. We got an ID. We got a title. We got a message schema. Messages, ID, conversation, role, content, tool calls, tool results.\u003C/p>\n\u003Cp>Okay. So this is the type of response we're getting back. I'm just gonna save that. Right? Alright.\u003C/p>\n\u003Cp>That was the payload that we sent. What is the response that we got? Okay. And now we got twelve minutes left just to, like, try to round this out and see if we can get this persistence part of it. Alright.\u003C/p>\n\u003Cp>So we're gonna create conversations. This is gonna be a UUID for the ID, created at for the time stamp, created by, updated at, updated by I will just go with what AI said here. I really don't wanna think too much about this. We'll do a mini to one relationship here. We'll call that the user.\u003C/p>\n\u003Cp>The related collection we're gonna use is directus underscore users. It'll show a link to the item. Great. And then we're gonna go in and create, what, messages. Messages.\u003C/p>\n\u003Cp>These are gonna be what did it say there? That's a UUID. Yeah. UUID. Conversation?\u003C/p>\n\u003Cp>Yes. I'll link those together. Content. Oh, not created at. Created at for the time stamp.\u003C/p>\n\u003Cp>Created at. K. We're gonna have a role for that. Role is gonna be what? That could be a drop down, so we could set that up as a drop down if we wanted to.\u003C/p>\n\u003Cp>User, that's gonna be user, or assistant. Too many a's. Assistant. Too many s's. Too many s's.\u003C/p>\n\u003Cp>Alright. And then we have the message content, which I I think is gonna be marked down, content. We could say tool calls, tool calls, Tool results. I don't know if this is actually gonna be it or not. Tool results, ID, conversation.\u003C/p>\n\u003Cp>Then we're just gonna link this together. I add a mini to one relationship here. Conversation. The related collection is gonna be conversations. We're gonna show a link.\u003C/p>\n\u003Cp>And what I'm gonna do, I almost always go into the advanced field relationships or the advanced, advanced mode when creating fields. I'm gonna add the reverse in here. So I'm gonna add all the messages to this conversation. Great. And okay.\u003C/p>\n\u003Cp>So now create conversation, load conversation, save a message, use chat persistence. Okay. Yeah. Alright. Let's just roll with it.\u003C/p>\n\u003Cp>This is gonna create a new composable. Use chat persistence. Sometimes it's fun just to shut your brain off and totally, just not think about it. It's fun that this show, honestly. Like, how fast can we turn something?\u003C/p>\n\u003Cp>So let's go back to our module. We're gonna import this. The actual import is just gonna be from use chat persistence. And we're not there is no handle input change, right? Handle submit.\u003C/p>\n\u003Cp>Load conversations. Handle submit. Use chat. Stream protocol. Save message.\u003C/p>\n\u003Cp>V model input, we do have inputs. We need to import on mounted watch from view. Use chat persistence. What's that gonna be? Dot JS.\u003C/p>\n\u003Cp>Or is that just like a default? Nope. Export function, use chat persistence. Why is it not picking that up? Oh, because it's in the same directory.\u003C/p>\n\u003Cp>Duh. Alright. Seven minutes remaining. Let's see what happens now. AI on top of AI, building AI.\u003C/p>\n\u003Cp>How can you help me? Alright. So we'll just watch the network request here. That's finished. I don't see another network request.\u003C/p>\n\u003Cp>Oh, maybe you gotta refresh first. How can you help me? Oh, boy. I'm not sure if you can see what's going on here, but we are going to be bricking this instance very quickly if we continue that. So, conversations, right, what do we do here?\u003C/p>\n\u003Cp>We've created some type of infinite loop. Thank you, AI. Love it. We're gonna load conversations on that. Watch conversations.\u003C/p>\n\u003Cp>Where are we getting the ID? Oh, why are we passing it? Yeah. Why do we want we don't want that. Is that what it is?\u003C/p>\n\u003Cp>The save message Is that where the problem came in at? I don't know. It's saving the same ID test. Nope. Still there.\u003C/p>\n\u003Cp>Alright. So you messed up, bro. You introduced an infinite loop. Why did you do that? Is it somewhere in our composed load message?\u003C/p>\n\u003Cp>Handle submit function calls itself recursively. Yeah. Okay. Yeah. Okay.\u003C/p>\n\u003Cp>Okay. Well, what did you change? Handle submit, await, append using Alright. Now is it possible to nuke all of these? Let's just nuke all these messages.\u003C/p>\n\u003Cp>Right? I'm gonna go straight to the database. Boom. Boom. Boom.\u003C/p>\n\u003Cp>This is fun. I like messing around with this AI stuff. We're just gonna nuke all of these conversations. There's 80,000,000 conversations as well. Maybe now this will actually work.\u003C/p>\n\u003Cp>Oh, some type of error handling there. It just disappears. Hey. Okay. So now we should be getting back to where we need to be.\u003C/p>\n\u003Cp>Messages. There we go. It's not saving the conversations correctly. Conversation. Current conversation.\u003C/p>\n\u003Cp>What is the current conversation? Current conversations dot ref. Well, no. There we go. We're creating a conversation.\u003C/p>\n\u003Cp>So current conversation dot value, save message, current conversation dot value. So why isn't it creating a conversation? Let's say create conversation. So we're gonna create a conversation when we load this. And maybe that'll fix the persistence issue.\u003C/p>\n\u003Cp>There's the conversation. Test message. Some type of stream issue going on there. But if we now go to conversations. No.\u003C/p>\n\u003Cp>Gremlins in the AI. This is why you don't have AI do your coding for you. Super helpful, though. Right? No chance I would have gotten here as far as we have in an hour without this tool.\u003C/p>\n\u003Cp>Obviously, lots of bugs and interface issues to squash before that. What's how many deals do we have outstanding? Let's try a simpler query. Deal stage ID. Are there deals?\u003C/p>\n\u003Cp>I thought there were deals. And maybe it doesn't know enough about the it just doesn't know enough about it. I know I've introduced some bugs. I kinda wanna end on a high note here, but we're running out of time. Right?\u003C/p>\n\u003Cp>We're gonna call it good. Let's end on a good one. What kind of collections do we have here? Organizations deals. You could probably feed it, like, the current user as well.\u003C/p>\n\u003Cp>That's gonna be, like, accountability, maybe. I don't know. How many open task activities activities are there? Structure of the activities collection. Man, I would just ruin this.\u003C/p>\n\u003Cp>How many orgs are there? I can't even add another working example. Sometimes that's how these things go. How many organizations are there? Yeah.\u003C/p>\n\u003Cp>We're now getting, like, crazy mad errors that we broke something. Alright. So there it is. Some type of weird infinite recursion glitch that I have introduced by just leveraging AI and tabbing and accepting. So now there's a lot of debugging involved in this, but this is a great example of the power of Directus, the power of AI, Vercel AI SDK, whoever's working on this, if you watch this, amazing work on this to make, this sort of stuff really easy and accessible and quick to pick up.\u003C/p>\n\u003Cp>Boom. Goes the dynamite. That's it for this episode of 100 apps, one hundred hours. I hope you will stay tuned for the next episode. Ciao.\u003C/p>","Welcome back to another episode of 100 apps, one hundred hours. I'm your host, Brian Gillespie, for Directus. And today is a very ambitious hour. I've got no clue how this is actually gonna go, but we are going to try and build an AI copilot that sits right alongside our data, inside our back end, which is Directus. Alright. If you're new to the show, 100 apps, one hundred hours, we have sixty minutes to plan and build an application, a clone of Airbnb, did some something we can actually ship and pat ourselves on the back at the end of the day. Or, you know, more often times than not, we struggle and then we, fail publicly. You know, sometimes that stinks, but it's all in good fun. The second rule of 100 apps, one hundred hours is use whatever you have at your disposal. So, AI tools, past projects, you know, if I my kids could code at this point, I'd probably leverage them. But, it should be a fun episode today. Let's dive in and try and figure out what we're actually doing. Let's put sixty minutes on the clock. Boom. AI Copilot go. So, at this stage of the game, AI is very impossible to ignore, and, you know, chatbots are everywhere, very commonplace. You know, lots of folks using Claude, chat GPT on a daily basis. Even my kids are aware of what it is. So, what we're gonna do is try to build an AI Copilot into our Directus instance. So I've got this blank Directus instance. I'm imagining this as a module. Directus is super flexible as a back end in the CMS because we got full control over what's going on. I'm imagining it as a a module with, like, a standard chat interface. I don't know if we'll get there or not, but let's just discuss functionality. Right? We want to be able to chat with AI models, specifically LLM models. Cat. You have to actually learn to spell Bryant. Alright. We wanna be able to chat with the LLMs. We want to be able to use tool calling. I think that's the technical term to fetch Directus data. Data stored in Directus. Data stored in Directus. We wanna do this do all this through a beautiful chat interface. Alright. That's our functionality. Right? We're gonna probably have as far as our data model, we'll have, like, a conversations to, like, persist these. We've also got our data, which I'm going to leverage some of our existing, like, starter templates. Great. Okay. So there's the arrows drawn in the sand. Let's let's dive into this thing. The first thing I'm gonna do, I'm gonna load up some sample data. So what I wanna do is go in here and create a token. I could just use my standard password as well, but we're just gonna copy that out. This is set up at local host 8055. I'm gonna come into the terminal, open up a new terminal instance here, and once that spins up, I'm gonna do directus m p x. I guess I can make this big so we can actually see it. Right? Directus template CLI at latest. Apply is the command I'm gonna run, and this should load up some templates for me or the ability to load a template. Fetching. Okay. So we're gonna apply a community template. Looks great. Let's do let's just do our simple CRM for this one. You know, I could the simple website CMS, super excited about that template. I love that one. For, like, a assistant, maybe CRM makes more sense. So we're defaulting to eight zero five five. I'm just gonna add my token in there, and you could probably steal this token. This instance is gonna be destroyed, and I'm not sure I'm I'm sure somebody could figure out how to get access to my local host. I know we got a lot of really great hackers out there in the community. That template was applied successfully. And now I'm just gonna hit reload, and the first thing I see is Michael Scott there. Alright. So now I've got some sample data to at least work with. We got some organizations. Great. So let's dive into how we're actually gonna pull this off. If I map this out, basically, what we're gonna take advantage of are extensions in Directus. Extensions, extensions, extensions. Two types specifically. Right? So for secure communications to our LLMs, we want to have a API proxy endpoint. So we're gonna set up an endpoint, and I'm pretty confident we can get this done. The actual chat UI, I'm not sure that will be what we call a module. So a module is basically just whatever functionality that you want. We give you tools like composables to access data inside the Directus instance, and then we give you free reign. So, like, a good example of a module is our command bar or command pallet. This is an extension that you can install from the marketplace. What it does is it adds a command k search to every bit of Directus so I can quickly search no matter where I'm at, whether I'm on contacts and I wanna search organizations, blah blah blah. It also comes with, like, a a settings page, and it's MIT licensed. So, you know, if you are looking at building, like, a custom module, this is a good one to copy because it has a ton of functionality and, it had a small hand in it. Kudos to Hannes from our team for, dude, just making this freaking wicked awesome one. Alright, but back to the task at hand, we're going to create a custom endpoint for this. So locally, I'm not sure if you can see this or not, we've got our databases here and then we've just got this Extensions folder. There's the Registry, which is when you install extensions from the marketplace. We're just going to create a new extension for this. So I'm going to open up this inside the terminal somewhere here, get this back to a reasonable level where I can actually develop, and I'm gonna do npx create create directus extensionlatest. Just look at the screen, I'm terrible at narrating. We'll go ahead and install this. It's been a minute since I've done this. And we offer another type of extension where it is, which is called a bundle. So a bundle is basically wrapper, like we're building a ChatGPT wrapper. A bundle is essentially a wrapper where we can, like, bundle some extensions together. That's a bundle. Right, we can distribute an endpoint and a module within a bundle, that's what we're gonna do here. So I'm gonna scroll all the way down, we're gonna do bundle, we're gonna call this Directus, let's just call it AI Copilot, that's what we'll call it. Yes. We wanna auto install dependencies. And now inside our extensions folder, you can see Directus is fleshing this out for us. We've got the Directus Extensions SDK, and then we can see there's nothing else in here. Right? So the next thing that we wanna do is CD Copilot, and we're going to hit npm run add so that we can add an extension to this bundle. Yeah. The if you're not using a bundle, like, it should automatically initiate the extension type for you. But in this case, we want to do an endpoint. We're going to call this Chat Endpoint. Let's use TypeScript and this will create a new Directus endpoint for us. Very simple, You'll notice a Request Response here behind the scenes, just an Express app. And next, we're going to talk about what we're actually going to use. Giving myself a seizure here, switching back and forth. So, I've been experimenting in other projects with the AI SDK from Vercel. That's what we're gonna use here and try to get as far as we can and try to make this really nice in a short period of time. So let's fire this up, right? Docs. What do we need to install first? Right? Do I want to use OpenAI or Anthropic? I've got I think I've got an Anthropic API key locally already. Do I do I not do, EMP? Yes. I do have a API key locally. Great. So let's do that. Right? We are going to do an overview. We want to install PMPMIAI. We're gonna use Anthropic as a provider, AI SDK Anthropic. And because we're using Express here, we're gonna wanna import Express as well, or install Express. We're probably also going to wanna have Zod. Right, if we get into, like, the tool calling, Zod is a big one, so we'll add Zod. That should give us the tools that we need to work on this, and let's just look at our custom endpoint now, right. The next thing I'm going to do is hit pnpm dev inside this. This will set up a watcher for our extension, and I'm just going to stop our container and restart just so it picks up that extension now that we've built it. And what this is gonna do, it should now be available at slash chat endpoint. Let's see. Local host. So if I refresh, we should see that extension. There's our chat endpoint. Let's hunt down the URL for this. Chat endpoint. Hello, world. There it is. Boom. Boom. Boom. Great. So how can we control that if we want? Set up some of this documentation here. Alright. So the the next thing that we're going to want to specify, I could specify, like, an endpoint or an ID for this when we define the endpoint. Right? If I put these up side by side, export defaults. We're gonna call this the chat. Chat. We're gonna have a handler for this. And in the handler, we'll provide the router and the context. Alright. Now we save. This should reload because I've got that set. And now if we load this again, chat endpoint doesn't exist, but now chat will. Right? Great. So now we can control what we want that endpoint to show up as. Awesome. Alright. What's next? Right? We're going to actually pull this in and set this up. Alright, so a couple of things that we're going to do that you probably noticed here, right? If we want to pull the API key from our environment, we're gonna want to actually pull that out, right. So let's do Destructure This. We're also gonna do we actually need that? No. Let's just keep it simple for now. Let's look at this AI SDK, and we're gonna import createanthropic, and we're going to create an anthropic client. Create an anthropic API key. We're gonna leave the base URL the same. Great. I'm not sure why we're duplicating that. Right. We're gonna add a router. This is gonna be a post method. So let's now take a look at was it did I have a I have an express option in here? Okay. So here's what a typical express setup might look like. So we're gonna import z from za. We'll go ahead and import za. Let's also do stream text from AI. We're gonna use router dot post, and let's see what AI comes up with. Message request dot body stream twenty twenty. Let's see the newer model there. Provider instance, anthropic provider. Does this list out their models? Okay. Yeah. Here's the latest model for Claude SONNET. That's what we're gonna use in this case. And we got messages, role, user content, message. We're just gonna send the stream. What is the is there a helper inside here for this? Result dot stream. Results dot pipe stream to response. Let's call this the result. Alright. And let's see if this is actually going to give us what we want. Alright. So we post a message. This should send something to the AI. I'm just gonna go into hundred apps. We're gonna create a new request. We'll say HTTP local host eight zero five five slash chat. Oh, that's the name and not the actual URL. Chat. Alright. We got a body. We're gonna create a JSON body. It's gonna have a message. What is two plus two? Send HTTP request wrong version number or prototype blah blah blah blah blah blah. Maybe we should wrap this in a try catch, see what we got. Oh, clean that up for me. Oh, well, no. No. No. No. No. Try catch console .log dot error. Do we have formatting? I do have some formatting enabled in this. Let's just try to send the results. Pres that's in result. Extension's reloaded. What is this showing? StreamText. Create anthropic API key. Console dot log. Do we have the actual ENV? We do have the ENV. Did I name it the correct thing? Oh, god. What an idiot. HTTPS should be HTTP base stream, send result. I don't know that the actual Bruno here will, like, handle streaming. What if we just tried, like, generate text? Generate text. Awaits. Let's just make sure this is gonna actually work first before we get crazy with it. Generate text results dot send. Result. Alright. Let's pull chat over here. What's two plus two? We hit send. Okay. What is two plus two? Finished reason, two plus two equals four. There's the text we get back. Okay. So now we've got, like, some type of AI proxy going on. Right? Let's jump to, like, tool calling at this point. We have, like, forty minutes left. You know, I read through the documentation for this. Not sure we're gonna get this correct, but alright. This may be where we just lean on AI to build AI and call it good. What I'm gonna do let's just copy all of this. Right? And this wouldn't be fun if we didn't, like, make it super meta and have AI create AI. Let's say something like this. Here's the documentation for tool calling with Vercel AI SDK in Node. Create a tool for fetching data from the Directus instance using the items service. Alright. So when it comes to, like, custom endpoints in Directus, one of the things that you do have access to is all the services that we use internally as in Directus, and, hopefully, this is gonna do a somewhat decent job of that. We'll kinda see what it comes up with. But if I take a look at, like, the extensions docs and we go to extension services, like, how do I access items? Right? Whenever I define a custom endpoint, we receive the, you know, router instance that we can plug into, and then we get this context. So the context has the EMV, has our services, it has this helper function for getting the schema. And then we define a new item service based on the collection that we're gonna call. Right? Let's see what we come up with. So I'm just gonna hit apply. We'll kinda go through this. We got stream text, generate text, item service, this looks okay. We're also gonna need schema, services get schema. We're gonna need that as well. Alright. So what is this actually doing? It's creating tools, multiple rounds, query from a a Directus collection. What's the filter? Object equals await get dot schema. Request dot accountability. Can we just leave that part off? That will be read by query, filter, success, return the items, the count, accept the file, await generate text. Something is off here. We got, like, one too many. Where are we going wrong? There we go. Okay. Alright. So, hopefully, if we invoke this tool, there's a description. Now let's try a new message, and let's just ask it let's pull up Directus. Right? Simple question. Right? Like, how many organizations do we have? How many organizations are in our Directus instance? There are six organizations in the Directus instance. These are Health Plus, Technovus, Solartec, Tesla. Boom. So nailed it spot on. We can see kind of what's going on here. There's the context that that we provided, I guess. I could see, like, the answers there. That's great. I'll help you. Okay. So there's the actual organizations. Cool. So now we've got kind of this tool calling functionality into this endpoint, which is pretty nice. Do we want to make it aware of the schema of Directus? I'm not sure, like, is this giving it enough information? Or tools, query collection. What does our actual schema look like? Right? Console log, await. Console schema equals await git schema. Oh, it's gonna be a async handler. Hansel dot log schema. We'll just stop logging that. Directus flows, schema, deals, activities, deal contacts. Yeah. So the other thing to note that this could get expensive pretty quickly. Alright. Okay. So now we are cooking at least with something. Right? Let's try to go in and focus on, like, this chat interface. Right? Now how are we going to do that? We're going to go back to our extension. I'm going to disable this for now. We're going to hit npm run add. We want to add a module for this. So this will give us a, we'll call this a Copilot module. Chat module, whatever. There we go. So this is gonna add another folder within our AI Copilot. So now we got the chat endpoint. We've got the Copilot module. And within this module, now if we just build this, we should see this module becoming available in our settings for the project. Custom. Custom module. There we go. So I can actually define this. Right? So each module or a lot of the interface extensions have this index dot ts. We're gonna call this our Copilot module. Let's call it Directus AI Copilot Magic. Is it gonna be Magic? Oh, there's not a Magic icon. What do we have as far as icons? Designer OCD gets me every time. A recovering designer. What is that one called? Magic button. Magic button. See if that gets us where we wanna be. Discard. Refresh. There we go. Alright. So I'm gonna enable this. We're gonna move it up to the very tippy top. There's our module. Here's our custom content. Boom. Didi boom. It's just a simple view component. Right? We can get as complex or as detailed as we want. On the client side here, we have access to some composables. Right? So on the back end, like the the Node. Js side, we've got, like, this item service we can call. On our other extensions where's the extension types? There we go. Like interfaces, for example, accessing internal systems, we can access, like, stores through this. We can also access, like, the used API or the SDK. Great. We'll just pull in the API. And now let's look at the documentation. So by no means am I an expert here, and this is probably where things are gonna get pretty hairy. Like, if we recap, we've got a proxy endpoint. We've implemented just a tool to fetch Directus data and include that in our Copilot. Now we need to actually build this, like, this chat UI that we're so used to seeing, right? And Directus is using Vue, which I love Vue. Vue is amazing. Most of these AI, like, pre built chat UIs are React based. So if you know of a good one that's for Vue, for building chatbots really quickly, let me know. So let's go here to SDK UI. Right? They do have support for, like, Vue JS. I see that here, this use chat function. We're gonna need that AI SDK view, so we might as well go ahead and install that as well. AI SDK view. Okay. There's the API endpoints, conversational interface. Is there, like, a simple recipe for this? Stream checks with a chat prompt. Alright. So, again, this might be a great thing to lean on AI for using here's an re a React example from our the Vercel, SDK. Or, actually, let's just piggyback off that last chat. Let's see. Here's a React example from for chat UI from Vercel AI SDK. Oh, actually, I have to show that. Build a view version of this that works with our chat endpoint. There we go. See what AI comes up with. So it's creating another composable. Why are we just use the built in used chat from Vue AI SDK. How do I go back to that? Chatbot AI SDK, chatbots. That was what? Under the API reference? Use chat view. And I just yeah. SDK view. Okay. And now we're getting somewhere. We got some messages. We got v button, v text area. No. Alright. Let's let's try and look at what's going on here. Handle submit, use chat, use API, AI Copilot, message content. So here's our messages. The v text area, v button, these are built in components within Directus. It is using Tailwind, which we we don't use Tailwind to Directus. So, use vanilla CSS instead, And then we can hit save and see if this is actually gonna work. Right? Now, I will tell you, like, I I've been using Cursor a lot recording this season of a hundred apps, hundred hours. I like it a lot, especially for prototyping. I found myself definitely suspect of the code sometimes, so don't just tap, tap, tap, accept. Yeah. Be very deliberate. And before you ship anything to production, make sure you test all this and go back and understand what's going on. This is not going to work, obviously. I just know our variable syntax, all of these have dash theme, dash dash in there. So hopefully, this solves that, but those are the things that you gotta watch and sometimes where, like, the AI Copilot stuff can get in the way. Right? How many orgs, organizations are in Directus? There is no icon button here. What would that be? Send. Google material symbols. Is there a send? There is a send. We don't have a icon prop here as well, so we're gonna do something like this, like v icon name equals send. That should give us the icon that we're looking for. Yeah. There we go. Okay. How many organizations are in Directus? Alright. So is this actually gonna work? Who knows? We'll see. Chat is not coming back. Internal server error. There's the messages. I don't think we're gonna need to pass the ID of this. Right? So if we look for a unique identifier for the chat, let's not use that. We got slash chat. Where is the local host slash chat? Alright. If we look at our endpoint, right, we need to adjust this. Right? We're gonna do StreamText, and we want to do the result for that. So we're gonna pipe that stream. Messages, content dot messages. What is the example for this look like? Is there where's that node? Like, so much of development for me is diving through documentation and just bouncing back and forth. Express. You can set up an Express server, stream, generate text, stream text with a chat prompt. Stream object, stream stream text. You probably just wanna look at it like the actual APIs here. Stream text. What are we passing to stream text? We've got messages that we're gonna pass. So those are just gonna be coming from the body. Right? Those are gonna be messages. We'll just pass those messages. And is that gonna get us what we want? Tools is already there. Do we need, like, a max tokens? No. Alright. Let's just see how far this gets us. How many organizations are in Directus? Okay. There's the so we're seeing the response. I can see the actual response there, but I'm not seeing it show up in our actual module here. Right? Messages. Alright. Use chat. Append. Let's just test what they're doing. Yeah. So that's kind of what we want there. Append content inputs. Result to data stream response, system helpful assistance, messages, I don't know why we have handle input change. Set appends. Let's see what's actually going on. This is where things get tricky. Right? Is there a handle input change inside the use chat model? Use chat. Initial message, initial input on. Handle submit. What does this return? Handle submit. I don't think there is a handle input change. Right? So, again, this is where, like, things get dangerous with AI, where you take 10 giant steps forward, and then you take, like, six giant steps backwards just trying to figure out what's actually happening here. So, chatbots. Where's the core StreamText. Well, actually, let's look at use chat on this side. Right? Initial input, initial messages on tool call, on response. If set to stream protocol, stream protocol should be text, I guess. Let's see how many orgs are in Directus. Oh, there we go. Okay. Now we're getting somewhere. Right? Okay. So now we're seeing the text being streamed in. If I wanted to select this, I can't because of what we've got set up in Directus. But, eighteen minutes in, like, we've got chat with AI models. We've got tool calling. We are somewhat through. Like, maybe we strike through half of that. We do all this through a chat interface. Alright. Now alright. Let's dive into, like, persistence. What do you think of those names? What do you think of those names? Okay. Yeah. We should probably have some type of, like, markdown formatting here as well. Is that what's coming back from the actual API? Is it just markdown? Yes. Okay. So do we have do we have a markdown helper inside Directus? Directus dash Directus issues. Cisco markdown. Directives markdown. Can we access this directive? The markdown. The markdown. Or we're buying that as MD? BMD? BMD. Okay. So can we do this? VMD equals message.content. Obviously, we're gonna lose that. Hey, o. Yeah. There we go. Now we're in some formatting. Looking nice. Looking nice. Okay. Get specific fields from a collection. Here to help you work with direct to collections. What do we have inside the database? Alright. Contacts. Who is the coolest contact in our CRM? Each of these contacts has their own interesting qualities. Without knowing your specific criteria for coolness, it is hard to definitively say who is the coolest. Edon kind of sounds like Elon. This might be a playful reference. Who knows? Who knows? Alright. So this is super interesting. Right? Let's work on persistence. Let's work on persistence to our directus database, what would the best data model be knowing the structure for messages. So if we look at stream stream object, stream text, And, like, our endpoint here is just streaming back the pipe text stream two response. Is there, like, a pipe data stream? Yeah. There you go. That's how we could get the actual messages. Pipe data stream to response. Send message data object. Okay. And then we should be able to like, inside our module, we can use the stream protocol. Let's just set this and see see if this is gonna work as well. Hey, o. Okay. But now instead, what we're getting back is actually the message data as well, which is probably what we're gonna want. Alright. What did you come up with, friend? So we've got messages, conversations, messages. So this is actually trying to give us a direct to schema, which kinda scares me a bit. So we got a user conversations. We got an ID. We got a title. We got a message schema. Messages, ID, conversation, role, content, tool calls, tool results. Okay. So this is the type of response we're getting back. I'm just gonna save that. Right? Alright. That was the payload that we sent. What is the response that we got? Okay. And now we got twelve minutes left just to, like, try to round this out and see if we can get this persistence part of it. Alright. So we're gonna create conversations. This is gonna be a UUID for the ID, created at for the time stamp, created by, updated at, updated by I will just go with what AI said here. I really don't wanna think too much about this. We'll do a mini to one relationship here. We'll call that the user. The related collection we're gonna use is directus underscore users. It'll show a link to the item. Great. And then we're gonna go in and create, what, messages. Messages. These are gonna be what did it say there? That's a UUID. Yeah. UUID. Conversation? Yes. I'll link those together. Content. Oh, not created at. Created at for the time stamp. Created at. K. We're gonna have a role for that. Role is gonna be what? That could be a drop down, so we could set that up as a drop down if we wanted to. User, that's gonna be user, or assistant. Too many a's. Assistant. Too many s's. Too many s's. Alright. And then we have the message content, which I I think is gonna be marked down, content. We could say tool calls, tool calls, Tool results. I don't know if this is actually gonna be it or not. Tool results, ID, conversation. Then we're just gonna link this together. I add a mini to one relationship here. Conversation. The related collection is gonna be conversations. We're gonna show a link. And what I'm gonna do, I almost always go into the advanced field relationships or the advanced, advanced mode when creating fields. I'm gonna add the reverse in here. So I'm gonna add all the messages to this conversation. Great. And okay. So now create conversation, load conversation, save a message, use chat persistence. Okay. Yeah. Alright. Let's just roll with it. This is gonna create a new composable. Use chat persistence. Sometimes it's fun just to shut your brain off and totally, just not think about it. It's fun that this show, honestly. Like, how fast can we turn something? So let's go back to our module. We're gonna import this. The actual import is just gonna be from use chat persistence. And we're not there is no handle input change, right? Handle submit. Load conversations. Handle submit. Use chat. Stream protocol. Save message. V model input, we do have inputs. We need to import on mounted watch from view. Use chat persistence. What's that gonna be? Dot JS. Or is that just like a default? Nope. Export function, use chat persistence. Why is it not picking that up? Oh, because it's in the same directory. Duh. Alright. Seven minutes remaining. Let's see what happens now. AI on top of AI, building AI. How can you help me? Alright. So we'll just watch the network request here. That's finished. I don't see another network request. Oh, maybe you gotta refresh first. How can you help me? Oh, boy. I'm not sure if you can see what's going on here, but we are going to be bricking this instance very quickly if we continue that. So, conversations, right, what do we do here? We've created some type of infinite loop. Thank you, AI. Love it. We're gonna load conversations on that. Watch conversations. Where are we getting the ID? Oh, why are we passing it? Yeah. Why do we want we don't want that. Is that what it is? The save message Is that where the problem came in at? I don't know. It's saving the same ID test. Nope. Still there. Alright. So you messed up, bro. You introduced an infinite loop. Why did you do that? Is it somewhere in our composed load message? Handle submit function calls itself recursively. Yeah. Okay. Yeah. Okay. Okay. Well, what did you change? Handle submit, await, append using Alright. Now is it possible to nuke all of these? Let's just nuke all these messages. Right? I'm gonna go straight to the database. Boom. Boom. Boom. This is fun. I like messing around with this AI stuff. We're just gonna nuke all of these conversations. There's 80,000,000 conversations as well. Maybe now this will actually work. Oh, some type of error handling there. It just disappears. Hey. Okay. So now we should be getting back to where we need to be. Messages. There we go. It's not saving the conversations correctly. Conversation. Current conversation. What is the current conversation? Current conversations dot ref. Well, no. There we go. We're creating a conversation. So current conversation dot value, save message, current conversation dot value. So why isn't it creating a conversation? Let's say create conversation. So we're gonna create a conversation when we load this. And maybe that'll fix the persistence issue. There's the conversation. Test message. Some type of stream issue going on there. But if we now go to conversations. No. Gremlins in the AI. This is why you don't have AI do your coding for you. Super helpful, though. Right? No chance I would have gotten here as far as we have in an hour without this tool. Obviously, lots of bugs and interface issues to squash before that. What's how many deals do we have outstanding? Let's try a simpler query. Deal stage ID. Are there deals? I thought there were deals. And maybe it doesn't know enough about the it just doesn't know enough about it. I know I've introduced some bugs. I kinda wanna end on a high note here, but we're running out of time. Right? We're gonna call it good. Let's end on a good one. What kind of collections do we have here? Organizations deals. You could probably feed it, like, the current user as well. That's gonna be, like, accountability, maybe. I don't know. How many open task activities activities are there? Structure of the activities collection. Man, I would just ruin this. How many orgs are there? I can't even add another working example. Sometimes that's how these things go. How many organizations are there? Yeah. We're now getting, like, crazy mad errors that we broke something. Alright. So there it is. Some type of weird infinite recursion glitch that I have introduced by just leveraging AI and tabbing and accepting. So now there's a lot of debugging involved in this, but this is a great example of the power of Directus, the power of AI, Vercel AI SDK, whoever's working on this, if you watch this, amazing work on this to make, this sort of stuff really easy and accessible and quick to pick up. Boom. Goes the dynamite. That's it for this episode of 100 apps, one hundred hours. I hope you will stay tuned for the next episode. Ciao.","6ad08c15-e97a-4887-9cf2-8b0d5b78d137",[691],"031a705b-7238-4184-a19a-e3408398dff0",[],{"id":142,"number":143,"show":122,"year":144,"episodes":694},[146,147,148,149,150,151,152,153,154,155],{"id":138,"slug":696,"vimeo_id":697,"description":698,"tile":699,"length":700,"resources":8,"people":8,"episode_number":131,"published":701,"title":702,"video_transcript_html":703,"video_transcript_text":704,"content":8,"seo":705,"status":130,"episode_people":706,"recommendations":710,"season":711},"new-years-resolution-bingo-generator","1163992739","Bryant is joined by Marc and Alvaro with the goal of building a goal bingo app in just 10 minutes using the MCP.","1e2aef3d-30ec-4672-be1e-f439efe7e045",18,"2026-03-02","New Years Resolution Bingo Generator","\u003Cp>Speaker 0: Alright, viewers. Welcome to, yet another episode of 100 app, 100 I don't know. No. No. No.\u003C/p>\u003Cp>One app in ten minutes. Right? We are doing the remix version today where we have ten minutes to build and plan plan and build an amazing app clone, crazy suggestion, and I have no idea what we're gonna do. So the rules. Right?\u003C/p>\u003Cp>Ten minutes to plan and build. No more, no less. How we're gonna do that? We are going to use some, amazing tools that we have built into Directus. And then, rule number two, the anti rule.\u003C/p>\u003Cp>Use whatever you've got at your disposal. Today, I've got two awesome dudes at my disposal, mister Alvaro and Mark from our team here at Directus. No strangers to the VUE community. Welcome to the show, gents.\u003C/p>\u003Cp>Speaker 1: Thanks for having us, Bryant.\u003C/p>\u003Cp>Speaker 2: Thank you very much for the nice intro. Happy, to be here.\u003C/p>\u003Cp>Speaker 0: Yeah. Yeah. No. I'm super excited. Have you guys given any thought to what we're what we're gonna build?\u003C/p>\u003Cp>Speaker 2: I think Mark has some idea though.\u003C/p>\u003Cp>Speaker 1: Yeah. So yesterday, we talked a little bit. I talked with Ava what we could build. And, I don't know if if I showed it to you, Brian, but on my website, I have a, instead of new year for solutions, I have new year's bingo cards. So you have five by five grid of stuff I want to do in the year.\u003C/p>\u003Cp>And if I get at least one in a row, so diagonal or horizontal or vertical, I already have bingo and it's a success. So I don't have to do all of them. And if you go to mark.dev/bingo\u003C/p>\u003Cp>Speaker 0: Okay. Let's check it out, guys.\u003C/p>\u003Cp>Speaker 1: You can it's still since it's just well, now February, not a lot has happened there. But\u003C/p>\u003Cp>Speaker 2: But it's a it's a really nice way to actually do some of the New Year's resolution. I always get the press at the end of the year like I have done, like, a quarter of them.\u003C/p>\u003Cp>Speaker 0: Yeah. I love it. Alright. So hey. This is neat, man.\u003C/p>\u003Cp>I I miss Yep.\u003C/p>\u003Cp>Speaker 1: And each of them can be either, like, you did it or you didn't do it or it can be progressive. Like, read six books and you are, like, one books, two books, three books in. And I think I also have, like, subtasks if we can make that work. Like, if one one, let's say, one bingo item has a few sub items as well. Like, don't have an example now, but that would also\u003C/p>\u003Cp>Speaker 0: be cool. Gotcha. Okay. New Year's resolution. Bingo card generator.\u003C/p>\u003Cp>Alright. That's what we're doing. This is gonna be amazing. This should be fun. What color are you guys feeling?\u003C/p>\u003Cp>Purple, pink?\u003C/p>\u003Cp>Speaker 2: I go I go purple. Blue. Oh, purple.\u003C/p>\u003Cp>Speaker 0: Purple. There we go.\u003C/p>\u003Cp>Speaker 1: Direct is purple. Nice.\u003C/p>\u003Cp>Speaker 0: Direct is purple. Alright, guys. Alright. So I'm sure you've seen the show. We're gonna start the clock.\u003C/p>\u003Cp>We got ten minutes to plan and build this thing. Let's do it. Alright. So, the first thing I usually do here is cover requirements. Right?\u003C/p>\u003Cp>So what are the requirements we need out of this? Right? We need to generate bingo cards. Like, what do you what were you calling those?\u003C/p>\u003Cp>Speaker 1: Like, items probably or\u003C/p>\u003Cp>Speaker 0: Okay.\u003C/p>\u003Cp>Speaker 1: Goals. Yeah.\u003C/p>\u003Cp>Speaker 2: Items. Yeah. Like a grid of of items.\u003C/p>\u003Cp>Speaker 1: Mhmm. Yeah.\u003C/p>\u003Cp>Speaker 0: Alright. So we got some goals. Those are what kinda fields are you tracking on those? Just the name of the goal?\u003C/p>\u003Cp>Speaker 1: Yeah. A name description and then the status.\u003C/p>\u003Cp>Speaker 0: Status of the goal. Progress. Progress. Is it are you status and progress interchangeable?\u003C/p>\u003Cp>Speaker 1: Yeah. I guess if you like the if the progress is under percent, the status\u003C/p>\u003Cp>Speaker 0: Okay. Yeah. Yeah. Yeah. Got it.\u003C/p>\u003Cp>Okay. And then we've got we've got goals. You've got what? Items underneath the goals? What do you do?\u003C/p>\u003Cp>We want, like, subtasks, like, if it's\u003C/p>\u003Cp>Speaker 1: You you can have subtasks. Let's see if there's one that has subtasks. I don't remember.\u003C/p>\u003Cp>Speaker 0: Tasks. It's called test. Alright. So the tasks would\u003C/p>\u003Cp>Speaker 1: play into into progress as well. I guess.\u003C/p>\u003Cp>Speaker 0: Into goal. And then the task completed increases progress. Cool. Alright. And task needs what?\u003C/p>\u003Cp>Name? Description? No. Just name? Date date probably.\u003C/p>\u003Cp>Speaker 1: Maybe the, the item can have a a completed ad. Yeah. They've completed as well for the task for the, item on top. Yeah.\u003C/p>\u003Cp>Speaker 0: Alright. And then we we wanna try to get a front end set up for this as well. Yeah. Alright. And we need a front end to display the bingo cards.\u003C/p>\u003Cp>Alright. This could be a stretch in seven minutes now. Let's see how we do. Alright. So what are we using today?\u003C/p>\u003Cp>Right? We've got a blank directus project. We've got Claude Code over here. Let's dive into it. Alright?\u003C/p>\u003Cp>I'm going to I'm not sure what you guys have been coding with. I've been using Super Whisper. I dig it. Hi. How are you doing?\u003C/p>\u003Cp>Alright, guys. We are building a New Year's resolution bingo card generator. I'm gonna copy and paste the data model that we want. You have access to a direct assistance. I want you to create our schema for that.\u003C/p>\u003Cp>We're also going to be building a front end to display the bingo cards. Let me know what questions you have. Let's create a plan. Alright. So this is crunching the transcript for that right now.\u003C/p>\u003Cp>Cool. There we go. I'll just, copy and paste this. Hopefully, we'll get some something good out of it. And we're gonna ask Claude Code to plan.\u003C/p>\u003Cp>Alright. So now we've got the schema. So we've got the direct us MCP connected to this thing. And I I think you guys have had a chance to try this out already. Right?\u003C/p>\u003Cp>Speaker 1: Yeah. I think Avro has. I haven't.\u003C/p>\u003Cp>Speaker 2: Yeah. Play with it in the morning. It's gonna create the collections, the schemas for you.\u003C/p>\u003Cp>Speaker 0: Yeah. Alright. So it's got a fresh direction. No custom collections. Alright.\u003C/p>\u003Cp>And I can zoom in just a little bit more so we could see this. What is the plan? And this is probably one of my favorite parts about this thing where it will prompt you for questions. Direct us flow, that's what we wanna do there. Vanilla JS.\u003C/p>\u003Cp>Yeah. That's what we'll do. What do you guys think? Five by five grid? Four by four?\u003C/p>\u003Cp>Speaker 1: We we can do also four by four so we don't have to come up with 25 things.\u003C/p>\u003Cp>Speaker 0: Amazing. Right? We got five minutes left.\u003C/p>\u003Cp>Speaker 2: You you can say to the MCP, hey, cloud, get, your twenty twenty six, bingo.\u003C/p>\u003Cp>Speaker 1: Oh, that was cool.\u003C/p>\u003Cp>Speaker 0: Yeah. Yeah. Alright. Public read, that's fine. Anyone can view those.\u003C/p>\u003Cp>Cool. Alright. And now, hopefully, this thing should have a plan.\u003C/p>\u003Cp>Speaker 2: I wonder which resolutions Cloud Code could have.\u003C/p>\u003Cp>Speaker 0: I don't know. Let's see. We'll we'll spin that up in an in a\u003C/p>\u003Cp>Speaker 1: new find out.\u003C/p>\u003Cp>Speaker 0: Alright. Cool. Right? Here's the direct to schema. There's our it's gonna create a flow.\u003C/p>\u003Cp>It's gonna create the front end. Sounds good. Let's let's roll with it. Right? I don't know what we're actually doing other than just talking this through here, but, I'm curious to see just how this thing works.\u003C/p>\u003Cp>I've you know, of course, like, spent a ton of time testing and building the MCP, but I've not spent a ton of time using it with, the latest Opus four five model. Alright. So it is checked the existing schema. Now we are it should start implementing. Yes.\u003C/p>\u003Cp>Please just start jamming on here. And if I refresh, now we should see some collections start to come in to the direct instance. We should see some collections. Start to come into the direct assistance. There we go.\u003C/p>\u003Cp>Okay. Alright. Oh, nice. I was just worried that I did something wrong. So we got our goals.\u003C/p>\u003Cp>We got our tasks. Amazing. Right? Now I could go in. We could potentially create some new ones if we need.\u003C/p>\u003Cp>One of the things that I like about this is it, like it seems like the anthropic models do a better job of, like, actually putting together a cohesive form than than, like, the OpenAI wants. So it's going through creating relations and fields. Alright, guys. So in this other one, create, some New Year's resolutions for yourself, Claude. Alright.\u003C/p>\u003Cp>You guys have any more guidance for this thing?\u003C/p>\u003Cp>Speaker 1: They should follow this the smart principle, probably.\u003C/p>\u003Cp>Speaker 0: Follow the smart principle. What's the smart principle?\u003C/p>\u003Cp>Speaker 1: Now you got me. So it's like measurable, achievable.\u003C/p>\u003Cp>Speaker 0: I know what you're talking about now. Yeah. The smart goals. Yeah. Yeah.\u003C/p>\u003Cp>And include the add them to the goals and tasks inside.\u003C/p>\u003Cp>Speaker 1: For the for me, the most important one is always measurable. You have to be able to measure what you do. If not, you lose yourself.\u003C/p>\u003Cp>Speaker 2: You lose yourself. That's so funny. It's okay.\u003C/p>\u003Cp>Speaker 0: That is very poetic. I love it, man. Alright. So it looks like okay. Yeah.\u003C/p>\u003Cp>I was just making sure we've got the relationship created correctly there. Alright. It is going to so we got two claws going. We got two minutes here. Let's see.\u003C/p>\u003Cp>I can see their goals and tasks.\u003C/p>\u003Cp>Speaker 1: Alright.\u003C/p>\u003Cp>Speaker 2: This is the next development, man. Right? Resting to\u003C/p>\u003Cp>Speaker 0: the next development. Yeah. This thing is going to yeah. I need to enter YOLO mode so we can actually, have this thing not stop to do these calls. But, behind the scenes, right, it is building this progress calculator flow.\u003C/p>\u003Cp>And and flows are\u003C/p>\u003Cp>Speaker 2: Yeah.\u003C/p>\u003Cp>Speaker 0: A a nice piece of functionality. It can be a little time consuming to set up, like, complex flows via the UI. So having direct us put these together, is, yeah, definitely time saving. Right? That's probably, like, five, ten steps there.\u003C/p>\u003Cp>Yes. Create those items. Alright. Let's see what we've got. Are we gonna get to the front end for this thing?\u003C/p>\u003Cp>I don't know if we are, man. I should've had Bryant. Should've had, Claude do that first. It's connecting the operations. Claude,\u003C/p>\u003Cp>Speaker 1: you need\u003C/p>\u003Cp>Speaker 0: to go faster, my friend. Alright. So what are the what are the goals that Claude set for itself? This should be interesting.\u003C/p>\u003Cp>Speaker 2: Put that description, Steven.\u003C/p>\u003Cp>Speaker 0: I'll reduce average response latency by 20%, Achieve 95% task completion rate without clarification. What an interesting goal. Here's the the individual tasks. And, I'll\u003C/p>\u003Cp>Speaker 2: And that was the end there. The front end.\u003C/p>\u003Cp>Speaker 0: Now it's doing it. No. Let me open this test project up. Is it going to have enough time? Yes.\u003C/p>\u003Cp>Proceed. New Year's resolution. Bingo. Oh, no. We ran out of time.\u003C/p>\u003Cp>So close. MCP connection should have access. No need to set up. I think, you know, this was so close, guys. I'm just going to it's against the rules, but you know what?\u003C/p>\u003Cp>We can make up our own rules here. I am just going to give access here to see and see if this will actually finish. Of course. There it is, man. The API permissions got us.\u003C/p>\u003Cp>We could see the bingo card here. There's the individual tasks. Ten minutes, full working back end with permissions, so close to a working front end. It did\u003C/p>\u003Cp>Speaker 1: pretty cool.\u003C/p>\u003Cp>Speaker 0: This is this is very cool. Right?\u003C/p>\u003Cp>Speaker 2: Even even with the subtask because that that wasn't an extra thing. Like, now it's the only iteration. Like, put the progress in the front end and\u003C/p>\u003Cp>Speaker 0: Yeah. I'm very curious to see. Right? It's already got, it looks like it maybe did it miss some of the flows? Right?\u003C/p>\u003Cp>So the thing to take away here is obviously, like, you could build incredibly quickly with Directus and MCP, and this is not loading, probably because of my computer. Just hates running all these Docker containers locally. What is going on?\u003C/p>\u003Cp>Speaker 2: How many do you have?\u003C/p>\u003Cp>Speaker 0: There's probably, like, five or 10 running at the moment, like, different instances. And I'm sure if I, like, killed the camera, it would probably stop doing this. I don't I don't know what's going on here. Local host, 8055. I at least wanna end this episode on a high note and show something.\u003C/p>\u003Cp>Come on.\u003C/p>\u003Cp>Speaker 1: Are you you're, are you running open claw? That's\u003C/p>\u003Cp>Speaker 0: I'm not yet. No. No. Not yet. I did test that thing out.\u003C/p>\u003Cp>It's, it's definitely an interesting one. I'm not sure that it is, it's not all there yet. Alright. It it's my testing was, like, most things you're gonna have to oh, great. Now I'm locked out of the actual instance as well.\u003C/p>\u003Cp>Hey. What is going on? Local host. 8055. Yeah.\u003C/p>\u003Cp>My open claw testing was basically, is it you're just gonna have to invest a lot of time into it to get something amazing out of it.\u003C/p>\u003Cp>Speaker 2: You you saw it. Right? Yeah.\u003C/p>\u003Cp>Speaker 0: Yeah. Alright. So we can see the flows. Did they yeah. It actually connected the flow.\u003C/p>\u003Cp>So I'm just curious. Right. Just wanting to see. Right? Build a mastering five new programming frameworks.\u003C/p>\u003Cp>Let's say we completed this right now. Does this flow actually work? And So it it it\u003C/p>\u003Cp>Speaker 2: could increase the progress of the task of the goal.\u003C/p>\u003Cp>Speaker 0: It should. And, of course, doing a hard refresh here is not not a great idea. Alright. Well, gents, you know, I'm not sure whether to put a, like, a thumbs up stamp on this one. Thumbs up stamp.\u003C/p>\u003Cp>We can just do I think. Yeah. This was, I think we got most of the functionality\u003C/p>\u003Cp>Speaker 2: there. Good at dive.\u003C/p>\u003Cp>Speaker 0: We just didn't get, the front end all the way there.\u003C/p>\u003Cp>Speaker 1: Oh, Brian, you are lagging quite.\u003C/p>\u003Cp>Speaker 0: Of course, it did.\u003C/p>\u003Cp>Speaker 1: Is it done? I think you you get a a thumbs up, Brian, because it we got a working thing at the end, and you had the the grid showing everything with the progress. So I think you get a thumbs up.\u003C/p>\u003Cp>Speaker 0: Yeah. Alright, guys. My computer is struggling, so we are going to sign off for this episode. Mark Alvaro, I've heard a little rumor that there might be a podcast coming up, so I'm super excited for that. Thanks for joining me for this episode of one app in ten minutes.\u003C/p>","Alright, viewers. Welcome to, yet another episode of 100 app, 100 I don't know. No. No. No. One app in ten minutes. Right? We are doing the remix version today where we have ten minutes to build and plan plan and build an amazing app clone, crazy suggestion, and I have no idea what we're gonna do. So the rules. Right? Ten minutes to plan and build. No more, no less. How we're gonna do that? We are going to use some, amazing tools that we have built into Directus. And then, rule number two, the anti rule. Use whatever you've got at your disposal. Today, I've got two awesome dudes at my disposal, mister Alvaro and Mark from our team here at Directus. No strangers to the VUE community. Welcome to the show, gents. Thanks for having us, Bryant. Thank you very much for the nice intro. Happy, to be here. Yeah. Yeah. No. I'm super excited. Have you guys given any thought to what we're what we're gonna build? I think Mark has some idea though. Yeah. So yesterday, we talked a little bit. I talked with Ava what we could build. And, I don't know if if I showed it to you, Brian, but on my website, I have a, instead of new year for solutions, I have new year's bingo cards. So you have five by five grid of stuff I want to do in the year. And if I get at least one in a row, so diagonal or horizontal or vertical, I already have bingo and it's a success. So I don't have to do all of them. And if you go to mark.dev/bingo Okay. Let's check it out, guys. You can it's still since it's just well, now February, not a lot has happened there. But But it's a it's a really nice way to actually do some of the New Year's resolution. I always get the press at the end of the year like I have done, like, a quarter of them. Yeah. I love it. Alright. So hey. This is neat, man. I I miss Yep. And each of them can be either, like, you did it or you didn't do it or it can be progressive. Like, read six books and you are, like, one books, two books, three books in. And I think I also have, like, subtasks if we can make that work. Like, if one one, let's say, one bingo item has a few sub items as well. Like, don't have an example now, but that would also be cool. Gotcha. Okay. New Year's resolution. Bingo card generator. Alright. That's what we're doing. This is gonna be amazing. This should be fun. What color are you guys feeling? Purple, pink? I go I go purple. Blue. Oh, purple. Purple. There we go. Direct is purple. Nice. Direct is purple. Alright, guys. Alright. So I'm sure you've seen the show. We're gonna start the clock. We got ten minutes to plan and build this thing. Let's do it. Alright. So, the first thing I usually do here is cover requirements. Right? So what are the requirements we need out of this? Right? We need to generate bingo cards. Like, what do you what were you calling those? Like, items probably or Okay. Goals. Yeah. Items. Yeah. Like a grid of of items. Mhmm. Yeah. Alright. So we got some goals. Those are what kinda fields are you tracking on those? Just the name of the goal? Yeah. A name description and then the status. Status of the goal. Progress. Progress. Is it are you status and progress interchangeable? Yeah. I guess if you like the if the progress is under percent, the status Okay. Yeah. Yeah. Yeah. Got it. Okay. And then we've got we've got goals. You've got what? Items underneath the goals? What do you do? We want, like, subtasks, like, if it's You you can have subtasks. Let's see if there's one that has subtasks. I don't remember. Tasks. It's called test. Alright. So the tasks would play into into progress as well. I guess. Into goal. And then the task completed increases progress. Cool. Alright. And task needs what? Name? Description? No. Just name? Date date probably. Maybe the, the item can have a a completed ad. Yeah. They've completed as well for the task for the, item on top. Yeah. Alright. And then we we wanna try to get a front end set up for this as well. Yeah. Alright. And we need a front end to display the bingo cards. Alright. This could be a stretch in seven minutes now. Let's see how we do. Alright. So what are we using today? Right? We've got a blank directus project. We've got Claude Code over here. Let's dive into it. Alright? I'm going to I'm not sure what you guys have been coding with. I've been using Super Whisper. I dig it. Hi. How are you doing? Alright, guys. We are building a New Year's resolution bingo card generator. I'm gonna copy and paste the data model that we want. You have access to a direct assistance. I want you to create our schema for that. We're also going to be building a front end to display the bingo cards. Let me know what questions you have. Let's create a plan. Alright. So this is crunching the transcript for that right now. Cool. There we go. I'll just, copy and paste this. Hopefully, we'll get some something good out of it. And we're gonna ask Claude Code to plan. Alright. So now we've got the schema. So we've got the direct us MCP connected to this thing. And I I think you guys have had a chance to try this out already. Right? Yeah. I think Avro has. I haven't. Yeah. Play with it in the morning. It's gonna create the collections, the schemas for you. Yeah. Alright. So it's got a fresh direction. No custom collections. Alright. And I can zoom in just a little bit more so we could see this. What is the plan? And this is probably one of my favorite parts about this thing where it will prompt you for questions. Direct us flow, that's what we wanna do there. Vanilla JS. Yeah. That's what we'll do. What do you guys think? Five by five grid? Four by four? We we can do also four by four so we don't have to come up with 25 things. Amazing. Right? We got five minutes left. You you can say to the MCP, hey, cloud, get, your twenty twenty six, bingo. Oh, that was cool. Yeah. Yeah. Alright. Public read, that's fine. Anyone can view those. Cool. Alright. And now, hopefully, this thing should have a plan. I wonder which resolutions Cloud Code could have. I don't know. Let's see. We'll we'll spin that up in an in a new find out. Alright. Cool. Right? Here's the direct to schema. There's our it's gonna create a flow. It's gonna create the front end. Sounds good. Let's let's roll with it. Right? I don't know what we're actually doing other than just talking this through here, but, I'm curious to see just how this thing works. I've you know, of course, like, spent a ton of time testing and building the MCP, but I've not spent a ton of time using it with, the latest Opus four five model. Alright. So it is checked the existing schema. Now we are it should start implementing. Yes. Please just start jamming on here. And if I refresh, now we should see some collections start to come in to the direct instance. We should see some collections. Start to come into the direct assistance. There we go. Okay. Alright. Oh, nice. I was just worried that I did something wrong. So we got our goals. We got our tasks. Amazing. Right? Now I could go in. We could potentially create some new ones if we need. One of the things that I like about this is it, like it seems like the anthropic models do a better job of, like, actually putting together a cohesive form than than, like, the OpenAI wants. So it's going through creating relations and fields. Alright, guys. So in this other one, create, some New Year's resolutions for yourself, Claude. Alright. You guys have any more guidance for this thing? They should follow this the smart principle, probably. Follow the smart principle. What's the smart principle? Now you got me. So it's like measurable, achievable. I know what you're talking about now. Yeah. The smart goals. Yeah. Yeah. And include the add them to the goals and tasks inside. For the for me, the most important one is always measurable. You have to be able to measure what you do. If not, you lose yourself. You lose yourself. That's so funny. It's okay. That is very poetic. I love it, man. Alright. So it looks like okay. Yeah. I was just making sure we've got the relationship created correctly there. Alright. It is going to so we got two claws going. We got two minutes here. Let's see. I can see their goals and tasks. Alright. This is the next development, man. Right? Resting to the next development. Yeah. This thing is going to yeah. I need to enter YOLO mode so we can actually, have this thing not stop to do these calls. But, behind the scenes, right, it is building this progress calculator flow. And and flows are Yeah. A a nice piece of functionality. It can be a little time consuming to set up, like, complex flows via the UI. So having direct us put these together, is, yeah, definitely time saving. Right? That's probably, like, five, ten steps there. Yes. Create those items. Alright. Let's see what we've got. Are we gonna get to the front end for this thing? I don't know if we are, man. I should've had Bryant. Should've had, Claude do that first. It's connecting the operations. Claude, you need to go faster, my friend. Alright. So what are the what are the goals that Claude set for itself? This should be interesting. Put that description, Steven. I'll reduce average response latency by 20%, Achieve 95% task completion rate without clarification. What an interesting goal. Here's the the individual tasks. And, I'll And that was the end there. The front end. Now it's doing it. No. Let me open this test project up. Is it going to have enough time? Yes. Proceed. New Year's resolution. Bingo. Oh, no. We ran out of time. So close. MCP connection should have access. No need to set up. I think, you know, this was so close, guys. I'm just going to it's against the rules, but you know what? We can make up our own rules here. I am just going to give access here to see and see if this will actually finish. Of course. There it is, man. The API permissions got us. We could see the bingo card here. There's the individual tasks. Ten minutes, full working back end with permissions, so close to a working front end. It did pretty cool. This is this is very cool. Right? Even even with the subtask because that that wasn't an extra thing. Like, now it's the only iteration. Like, put the progress in the front end and Yeah. I'm very curious to see. Right? It's already got, it looks like it maybe did it miss some of the flows? Right? So the thing to take away here is obviously, like, you could build incredibly quickly with Directus and MCP, and this is not loading, probably because of my computer. Just hates running all these Docker containers locally. What is going on? How many do you have? There's probably, like, five or 10 running at the moment, like, different instances. And I'm sure if I, like, killed the camera, it would probably stop doing this. I don't I don't know what's going on here. Local host, 8055. I at least wanna end this episode on a high note and show something. Come on. Are you you're, are you running open claw? That's I'm not yet. No. No. Not yet. I did test that thing out. It's, it's definitely an interesting one. I'm not sure that it is, it's not all there yet. Alright. It it's my testing was, like, most things you're gonna have to oh, great. Now I'm locked out of the actual instance as well. Hey. What is going on? Local host. 8055. Yeah. My open claw testing was basically, is it you're just gonna have to invest a lot of time into it to get something amazing out of it. You you saw it. Right? Yeah. Yeah. Alright. So we can see the flows. Did they yeah. It actually connected the flow. So I'm just curious. Right. Just wanting to see. Right? Build a mastering five new programming frameworks. Let's say we completed this right now. Does this flow actually work? And So it it it could increase the progress of the task of the goal. It should. And, of course, doing a hard refresh here is not not a great idea. Alright. Well, gents, you know, I'm not sure whether to put a, like, a thumbs up stamp on this one. Thumbs up stamp. We can just do I think. Yeah. This was, I think we got most of the functionality there. Good at dive. We just didn't get, the front end all the way there. Oh, Brian, you are lagging quite. Of course, it did. Is it done? I think you you get a a thumbs up, Brian, because it we got a working thing at the end, and you had the the grid showing everything with the progress. So I think you get a thumbs up. Yeah. Alright, guys. My computer is struggling, so we are going to sign off for this episode. Mark Alvaro, I've heard a little rumor that there might be a podcast coming up, so I'm super excited for that. Thanks for joining me for this episode of one app in ten minutes.","0878a4f4-5836-4a78-b897-a8b4789c9e7c",[707,708,709],"851de303-b434-4092-8b15-fb6720b1e4c2","010213cf-c50a-4f60-b264-afb8fcf3e4b3","487b9dd3-5c6e-4894-bf34-a2bb1f6d2b64",[],{"id":134,"number":135,"show":122,"year":136,"episodes":712},[138,139,140],{"id":139,"slug":714,"vimeo_id":715,"description":716,"tile":717,"length":700,"resources":8,"people":8,"episode_number":158,"published":701,"title":718,"video_transcript_html":719,"video_transcript_text":720,"content":8,"seo":721,"status":130,"episode_people":722,"recommendations":725,"season":726},"workout-app","1163990620","Bryant is joined by Matt with the goal of building a workout fitness app in just 10 minutes using the MCP.","54b6f2d5-ab15-4465-9cbe-1ddba413084f","Workout App","\u003Cp>Speaker 0: Alright. Alright. Alright. Welcome back to another episode of one app ten minutes. I'm your host Brian Gillespie.\u003C/p>\u003Cp>Today, we have got an extra special guest that I will introduce in a moment. But if you're new to the show, let's cover the rules. Ten minutes to plan and build and no more, no less. You animals couldn't sit for an hour, so here we are trying to build apps in ten minutes. Successfully or not, we will build the app or die trying.\u003C/p>\u003Cp>As for a special guest, right, mister Matt Miner from our marketing team at Directus. Matt, how are you, sir? Happy to have you on the show.\u003C/p>\u003Cp>Speaker 1: Well, I was good. In the intro, you said I was very special, and then you just said I was special. So which is it? Very special or just a special?\u003C/p>\u003Cp>Speaker 0: Very special?\u003C/p>\u003Cp>Speaker 1: Good. Extra special. Yeah.\u003C/p>\u003Cp>Speaker 0: How special do you wanna be? Just How\u003C/p>\u003Cp>Speaker 1: special do you wanna be? Enough. You know? No. It's great to be here.\u003C/p>\u003Cp>Unlike other guests, I had a little heads up, so you couldn't catch me off guard with a random meeting. So, I'm\u003C/p>\u003Cp>Speaker 0: Yeah. I mean, that's that's fun for me, certainly, to put people on the spot. The other thing that gives me a bit of anxiety is if you have had time to think on what we're actually gonna build Yeah. I certainly have no idea. You can thank Beth on our team for that.\u003C/p>\u003Cp>Thank you, Beth. I don't know if you're gonna watch this afterwards or in post production, but I appreciate you for putting me on the spot. Yeah. So, without further ado, you know, tell me what we're building so I can put it up here on the board.\u003C/p>\u003Cp>Speaker 1: Alright. We're gonna build. Little backstory. Since having our second kid, my son, I can count on the number I can count the number of times we've slept through the night on, like, one hand. And for one for one and a half years, he he turns two in June.\u003C/p>\u003Cp>So, for, you know, obvious reasons, like our I've not been to the gym. You know, I've not been working out. I get winded when I walk up the stairs. So I've been one of the New Year's resolutions this year has been getting back\u003C/p>\u003Cp>Speaker 0: to the gym.\u003C/p>\u003Cp>Speaker 1: I need some sort of, like, fitness app, workout app sort of thing, to track workouts and things like that, as well as keep, like, kind of a running diary. And, if we wanna get, like, crazy advanced with it, I have a lot of ideas for it. But, yeah, hopefully, this is something you can build and and I can sell on the side. So that's my goal today.\u003C/p>\u003Cp>Speaker 0: Jeez. Okay. I think a native app would be very difficult in ten minutes. We could certainly cook something up with Directus. So just to to prove to the folks there's no hand wavy magic here.\u003C/p>\u003Cp>Right? We've got a blank Directus back in. I do have cloud code set up for us with the direct Us MCP already connected, which is, hopefully going to help us move faster than the speed of light. But you know the drill. We're gonna start the clock, and then we'll plan this thing out and see how far we can get in ten minutes.\u003C/p>\u003Cp>Are you ready?\u003C/p>\u003Cp>Speaker 1: I think I'm ready.\u003C/p>\u003Cp>Speaker 0: He's born ready. Here we go. Alright. Ten minutes. Here we go.\u003C/p>\u003Cp>What sort of functionality do you want out of this thing?\u003C/p>\u003Cp>Speaker 1: Alright. So, basically, I need to be able to, like, put workouts in ahead of ahead of the, when I actually go work out. So I'm thinking, like, Monday, Wednesday, Friday, I'll go to the gym. I'll do, like, a push day on Monday, pull day on Wednesday, and, like, a legs day on Friday. Right?\u003C/p>\u003Cp>Within that, like, just a basic thirty minute workout, so it'll be kind of repeatable with the exercises. And then just having a way to log, like, the weight that I do. And then if if we wanna get extra fancy with it, like, implementing some sort of, like, AI increase of, like, you know, a couple percentage points of the weight. So, like, next time when I go, there's a little bit more weight. I have to be able to rate it.\u003C/p>\u003Cp>So, like, if I can hit all of the reps during the workout workout, and if so, then, like, increase it by, like, 5% or something weight wise. Yeah. Basic functionality is that. Logging, you know, planning the workout, with sets and reps in the workout and then logging, like, easy, medium, hard for each one.\u003C/p>\u003Cp>Speaker 0: Holy cow. Okay. Alright. No short order. Right?\u003C/p>\u003Cp>I'm not sure if you're using this tool yet or is it variety of tools? I've got this thing called Super Whisperer. I'm just gonna enable this because I suck at typing. Alright. And then I'm gonna talk.\u003C/p>\u003Cp>Hi, Claude. We are going to build a workout fitness app sort of thing for my friend, Matt. I'm gonna paste some notes on our data model, what the workout should look like and the different things that we need to, have inside the app. And I want you to quickly, as fast as you can, create a plan to add that to our direct us instance, using the MCP. Alright.\u003C/p>\u003Cp>So, we got a transcript here that would have taken me forever to type. I'm gonna paste this in. Oops. Let me make sure I copy this. Cool.\u003C/p>\u003Cp>And then let's go. Right? Now, normally, I would kick this over in plan mode, but we are extremely on the clock here. We got seven minutes and thirty seconds left. It should pick up the schema and existing collections from Directus.\u003C/p>\u003Cp>Speaker 1: For one shotting.\u003C/p>\u003Cp>Speaker 0: Is this We're we're just gonna have to one shot this. You know, I'm asking it to plan first, though. Right? I did say that. Quickly create a plan.\u003C/p>\u003Cp>So here we go. We got a plan. Matt's workout app. Alright. You got exercises.\u003C/p>\u003Cp>Cool. You got workouts, day of the week, target duration, workout exercises, junction table, workout logs, sets completed, exercise, default sets, default reps. Okay. So I see reps and stuff in there. We got the exercise progress.\u003C/p>\u003Cp>Exercise logs, that's where your your rating comes in, I guess. Optionally, create a flow. Alright. So let's answer these questions. Right?\u003C/p>\u003Cp>We want, LBs, an American LBs.\u003C/p>\u003Cp>Speaker 1: In our annoying metric system.\u003C/p>\u003Cp>Speaker 0: User User specific shared templates.\u003C/p>\u003Cp>Speaker 1: Context to that would be what? If, like, I'd go to the gym with my wife, we could share, like, the same workout.\u003C/p>\u003Cp>Speaker 0: The same workout app? Yeah. Maybe. User off, I guess. Let's do that.\u003C/p>\u003Cp>Specific exercises to prepopulate, come up with a list of exercises to populate. And, actually, we can have Claude do that separately. Rating scale, one to five or thumbs up or thumbs down?\u003C/p>\u003Cp>Speaker 1: I would say easy, medium, hard. So, like, if easy, progress weight. If medium, keep weight. If hard, reduce weight or reps.\u003C/p>\u003Cp>Speaker 0: If easy, increase, medium, increase?\u003C/p>\u003Cp>Speaker 1: Medium, keep the same. And hard, reduce.\u003C/p>\u003Cp>Speaker 0: Reduce reps. Alright. Five minutes. Let's see what it comes up with. It's gonna update the plan for us, and then, Has\u003C/p>\u003Cp>Speaker 1: it already been five minutes?\u003C/p>\u003Cp>Speaker 0: Yeah. This this goes quickly. Yes. Implement my guy. Go forth.\u003C/p>\u003Cp>What do you mean? So you could see already, like, the I I know how to click all these buttons and direct us, certainly. Right? I could go through, and I could do all this. And I've done it pretty quickly.\u003C/p>\u003Cp>But, AI is such a I it gives us so much leverage in this equation.\u003C/p>\u003Cp>Speaker 1: That's the thing, though. Right? Like, does anybody actually enjoy building out the data models, like, on the back end now? Like, this it's it's kind of fun, I guess, like, putting it's like putting a puzzle together, but, like, it feels better if you just get a foundation and then be able to, like, tweak on it. Right?\u003C/p>\u003Cp>I guess, posing that question to you, like, do you like building out the back end or do you care about the front end?\u003C/p>\u003Cp>Speaker 0: You know, I am I'm a results guy, man. Yes. You are. Do you know? I\u003C/p>\u003Cp>Speaker 1: You better get results.\u003C/p>\u003Cp>Speaker 0: Like the ship stuff. Yeah.\u003C/p>\u003Cp>Speaker 1: I I I need this.\u003C/p>\u003Cp>Speaker 0: On the spot. Like, I you know, I I am kind of sad on some level of, like, hey. It's not me doing the work anymore, but, you know, I've come to realize that, like, the the strength that I have is not actually writing the code or clicking the buttons. Right? It is putting the the stuff together.\u003C/p>\u003Cp>So we are at three forty three. What is Directus doing here? What is claw doing? Right? You can see it's creating fields for all the different relationships.\u003C/p>\u003Cp>It's adding those relationships. Now it's seeding the exercises. Yes. Let's create the default workout templates and create the flow to auto progress. So, again, this is this is a terrible way to build something for actual production in that, I've just got this set up in yellow mode, and, basically, I'm barely even reading these things.\u003C/p>\u003Cp>Yeah. We're I didn't even give you a chance to to comment on this. Right. But the the principles here are sound. Right?\u003C/p>\u003Cp>And that you've got a thinking partner in in something like Cloud Code or, you know, it could just be Cloud or Chad GPT. It doesn't really matter which AI you're using. It's It's helpful for thinking through these things. So if we take a look, right, now we've got our data model. We've got our exercise progress.\u003C/p>\u003Cp>We've got your different workouts with your different exercises already created here. Alright. You got your workout logs, which I'm assuming here's your workout.\u003C/p>\u003Cp>Speaker 1: So Monday. Yeah. And that's the planning of it. Right? It's like yeah.\u003C/p>\u003Cp>Monday. Well, probably\u003C/p>\u003Cp>Speaker 0: Oh, this is this is your yeah. The workout log. Workouts would be this is does this have your weight? No. It doesn't have the weights on it, does it?\u003C/p>\u003Cp>Yeah. So that part of it we're missing. Right? Maybe that is\u003C/p>\u003Cp>Speaker 1: And I guess we aren't specific about that. Yeah. Well, there's weight at least in the logs.\u003C/p>\u003Cp>Speaker 0: Yeah. So let's set up the Monday. Yesterday, Matt did this. Totally did it.\u003C/p>\u003Cp>Speaker 1: Totally did it.\u003C/p>\u003Cp>Speaker 0: I swear. Alright. So, you create your exercise log. You got a bench press, etcetera. Right?\u003C/p>\u003Cp>You completed five sets of five reps at two twenty five. Mhmm. Mhmm. Beast. Mhmm.\u003C/p>\u003Cp>Alright. So there you go. You got your exercise log. Let's check on what we're doing. Right?\u003C/p>\u003Cp>It is creating some operations now. So we should see, like, a flow in here. Should be creating these operations for it. Okay. We got a minute and twelve seconds left.\u003C/p>\u003Cp>Speaker 1: I should have not said my sob story at the beginning. We'd have more time. But, I mean, this looks great. Right? Like, I think what's really interesting about it is that, you know, not even using Directus as, like, the interface for this, like, just slapping together a lovable front end or something that's much easier and accessible, like, from my mobile device than just, like, while I'm there, just blah blah blah.\u003C/p>\u003Cp>Speaker 0: Yeah. Alright. So we're gonna go in, and we're gonna create this workout log again. It's Wednesday, pool day. Except you did it on Tuesday.\u003C/p>\u003Cp>It is we're gonna do hard incline. Oh, what's the pull? Lat pull downs. There you go. You had 20 sets.\u003C/p>\u003Cp>20 reps. Crushed it. 110 pounds.\u003C/p>\u003Cp>Speaker 1: I had my creosote\u003C/p>\u003Cp>Speaker 0: right there. Let's see what happens. User exercise progress. Did we did we get an error on the flow? Oh, we got five seconds left.\u003C/p>\u003Cp>Know. Out of time. No. Run scripts. Cannot read properties.\u003C/p>\u003Cp>Okay. So, you know, Claude screwed up a bit on our run script somewhere in here where, you know, it's it's wrote some JavaScript code, which is nice. One of the nice things about the flows inside Directus is you can just write arbitrary JavaScript and run that. But there's a a few debugging issues. Right?\u003C/p>\u003Cp>So ten minutes in this case. And, you know, let's let's take a look. Right? Let's go to the tally. We've got got the workout exercises.\u003C/p>\u003Cp>We got the reps. We got your Monday, Wednesday, Friday, and we got the ability to log the workout. We got the rating the workouts. What we did not get is the automatic increase. So that logic, we did not get.\u003C/p>\u003Cp>Speaker 1: And I would say that that So instead of the way it's interesting how I applied it to the full workout instead of the individual, like, exercises, which is probably why it got funky because it was probably waiting for us to put in all of the\u003C/p>\u003Cp>Speaker 0: like, hey. Was this individual exercise hard or not? That's a good insight. Right? So it does show you I I I think I'm impressed with how far you can get into\u003C/p>\u003Cp>Speaker 1: I mean\u003C/p>\u003Cp>Speaker 0: that's right.\u003C/p>\u003Cp>Speaker 1: Man, this is I mean, this is really the basis. You see a lot of apps out there like Fitbot or, like, Fitness AI, which are solid, but, you know, they're not customized to you. And being able to just, like, do this, get it custom, share it, you know, with whoever I'm working out with, it's powerful.\u003C/p>\u003Cp>Speaker 0: Yeah. Another ten, fifteen minutes, we could have had, you know, nano banana generate images of these or we could have copied images. Like, diagrams of how how to do the exercises. Right? Yeah.\u003C/p>\u003Cp>Could create workout templates. Again, like, you could have, like, auto progression or auto calculation on there. And then even with the AI chat inside Directus. Right? Mhmm.\u003C/p>\u003Cp>Hey, man. Plan me a new workout. Right? Even if you let's say you you haven't built that, that front end yet Yeah. The native app that we're gonna build in ten minutes.\u003C/p>\u003Cp>Yo. You could still interact with this on your phone\u003C/p>\u003Cp>Speaker 1: Yeah.\u003C/p>\u003Cp>Speaker 0: Because it is all mobile responsive. Right? And I can open up the AI chat inside here, add an OpenAI API key, and, you know, I've got somebody that can log all my workouts or create new workouts for me as well right inside Directus. I don't have to code anything.\u003C/p>\u003Cp>Speaker 1: Yeah. It's wild.\u003C/p>\u003Cp>Speaker 0: Yeah. Well Well You know? I mean I\u003C/p>\u003Cp>Speaker 1: we got far. We got farther than it would have taken me if I was gonna do this myself.\u003C/p>\u003Cp>Speaker 0: Well, we'll give this, like, the the fair to Midland. Like, meh, like\u003C/p>\u003Cp>Speaker 1: Yeah.\u003C/p>\u003Cp>Speaker 0: It's okay.\u003C/p>\u003Cp>Speaker 1: What is it? Gladiator? Don't know if it's up or down yet. I'll just give it this side. Don't know whether to kill it or let it live.\u003C/p>\u003Cp>Speaker 0: There you go. Yeah. The alright. Yeah. Well, Matt, thanks for joining me.\u003C/p>\u003Cp>This is fun. Yeah. I do like, this is something that I I need myself personally, so I do kinda wanna keep working on it on the slide.\u003C/p>\u003Cp>Speaker 1: Hey, man.\u003C/p>\u003Cp>Speaker 0: This is fun. Great idea, man.\u003C/p>\u003Cp>Speaker 1: Everybody's stealing my ideas lately. I'm just kidding. Give me access to this. I I wanna see you know what? We can also build a scoreboard so we can see, like, who can how much more I work with, bro.\u003C/p>\u003Cp>Speaker 0: Bro, I used to say\u003C/p>\u003Cp>Speaker 1: power I know. I I can't. I've seen you in person. There's no way. You're you got me beat.\u003C/p>\u003Cp>Speaker 0: Short and squat, dude.\u003C/p>\u003Cp>Speaker 1: See them. Alright. Cool. Well, thanks, man. Appreciate it.\u003C/p>\u003Cp>And, yeah, this is awesome.\u003C/p>\u003Cp>Speaker 0: Yeah. Thoroughly enjoyed it. Alright. Everybody out there, that is it for this episode of one app ten minutes. Thanks for joining.\u003C/p>\u003Cp>Stay tuned for more episodes.\u003C/p>","Alright. Alright. Alright. Welcome back to another episode of one app ten minutes. I'm your host Brian Gillespie. Today, we have got an extra special guest that I will introduce in a moment. But if you're new to the show, let's cover the rules. Ten minutes to plan and build and no more, no less. You animals couldn't sit for an hour, so here we are trying to build apps in ten minutes. Successfully or not, we will build the app or die trying. As for a special guest, right, mister Matt Miner from our marketing team at Directus. Matt, how are you, sir? Happy to have you on the show. Well, I was good. In the intro, you said I was very special, and then you just said I was special. So which is it? Very special or just a special? Very special? Good. Extra special. Yeah. How special do you wanna be? Just How special do you wanna be? Enough. You know? No. It's great to be here. Unlike other guests, I had a little heads up, so you couldn't catch me off guard with a random meeting. So, I'm Yeah. I mean, that's that's fun for me, certainly, to put people on the spot. The other thing that gives me a bit of anxiety is if you have had time to think on what we're actually gonna build Yeah. I certainly have no idea. You can thank Beth on our team for that. Thank you, Beth. I don't know if you're gonna watch this afterwards or in post production, but I appreciate you for putting me on the spot. Yeah. So, without further ado, you know, tell me what we're building so I can put it up here on the board. Alright. We're gonna build. Little backstory. Since having our second kid, my son, I can count on the number I can count the number of times we've slept through the night on, like, one hand. And for one for one and a half years, he he turns two in June. So, for, you know, obvious reasons, like our I've not been to the gym. You know, I've not been working out. I get winded when I walk up the stairs. So I've been one of the New Year's resolutions this year has been getting back to the gym. I need some sort of, like, fitness app, workout app sort of thing, to track workouts and things like that, as well as keep, like, kind of a running diary. And, if we wanna get, like, crazy advanced with it, I have a lot of ideas for it. But, yeah, hopefully, this is something you can build and and I can sell on the side. So that's my goal today. Jeez. Okay. I think a native app would be very difficult in ten minutes. We could certainly cook something up with Directus. So just to to prove to the folks there's no hand wavy magic here. Right? We've got a blank Directus back in. I do have cloud code set up for us with the direct Us MCP already connected, which is, hopefully going to help us move faster than the speed of light. But you know the drill. We're gonna start the clock, and then we'll plan this thing out and see how far we can get in ten minutes. Are you ready? I think I'm ready. He's born ready. Here we go. Alright. Ten minutes. Here we go. What sort of functionality do you want out of this thing? Alright. So, basically, I need to be able to, like, put workouts in ahead of ahead of the, when I actually go work out. So I'm thinking, like, Monday, Wednesday, Friday, I'll go to the gym. I'll do, like, a push day on Monday, pull day on Wednesday, and, like, a legs day on Friday. Right? Within that, like, just a basic thirty minute workout, so it'll be kind of repeatable with the exercises. And then just having a way to log, like, the weight that I do. And then if if we wanna get extra fancy with it, like, implementing some sort of, like, AI increase of, like, you know, a couple percentage points of the weight. So, like, next time when I go, there's a little bit more weight. I have to be able to rate it. So, like, if I can hit all of the reps during the workout workout, and if so, then, like, increase it by, like, 5% or something weight wise. Yeah. Basic functionality is that. Logging, you know, planning the workout, with sets and reps in the workout and then logging, like, easy, medium, hard for each one. Holy cow. Okay. Alright. No short order. Right? I'm not sure if you're using this tool yet or is it variety of tools? I've got this thing called Super Whisperer. I'm just gonna enable this because I suck at typing. Alright. And then I'm gonna talk. Hi, Claude. We are going to build a workout fitness app sort of thing for my friend, Matt. I'm gonna paste some notes on our data model, what the workout should look like and the different things that we need to, have inside the app. And I want you to quickly, as fast as you can, create a plan to add that to our direct us instance, using the MCP. Alright. So, we got a transcript here that would have taken me forever to type. I'm gonna paste this in. Oops. Let me make sure I copy this. Cool. And then let's go. Right? Now, normally, I would kick this over in plan mode, but we are extremely on the clock here. We got seven minutes and thirty seconds left. It should pick up the schema and existing collections from Directus. For one shotting. Is this We're we're just gonna have to one shot this. You know, I'm asking it to plan first, though. Right? I did say that. Quickly create a plan. So here we go. We got a plan. Matt's workout app. Alright. You got exercises. Cool. You got workouts, day of the week, target duration, workout exercises, junction table, workout logs, sets completed, exercise, default sets, default reps. Okay. So I see reps and stuff in there. We got the exercise progress. Exercise logs, that's where your your rating comes in, I guess. Optionally, create a flow. Alright. So let's answer these questions. Right? We want, LBs, an American LBs. In our annoying metric system. User User specific shared templates. Context to that would be what? If, like, I'd go to the gym with my wife, we could share, like, the same workout. The same workout app? Yeah. Maybe. User off, I guess. Let's do that. Specific exercises to prepopulate, come up with a list of exercises to populate. And, actually, we can have Claude do that separately. Rating scale, one to five or thumbs up or thumbs down? I would say easy, medium, hard. So, like, if easy, progress weight. If medium, keep weight. If hard, reduce weight or reps. If easy, increase, medium, increase? Medium, keep the same. And hard, reduce. Reduce reps. Alright. Five minutes. Let's see what it comes up with. It's gonna update the plan for us, and then, Has it already been five minutes? Yeah. This this goes quickly. Yes. Implement my guy. Go forth. What do you mean? So you could see already, like, the I I know how to click all these buttons and direct us, certainly. Right? I could go through, and I could do all this. And I've done it pretty quickly. But, AI is such a I it gives us so much leverage in this equation. That's the thing, though. Right? Like, does anybody actually enjoy building out the data models, like, on the back end now? Like, this it's it's kind of fun, I guess, like, putting it's like putting a puzzle together, but, like, it feels better if you just get a foundation and then be able to, like, tweak on it. Right? I guess, posing that question to you, like, do you like building out the back end or do you care about the front end? You know, I am I'm a results guy, man. Yes. You are. Do you know? I You better get results. Like the ship stuff. Yeah. I I I need this. On the spot. Like, I you know, I I am kind of sad on some level of, like, hey. It's not me doing the work anymore, but, you know, I've come to realize that, like, the the strength that I have is not actually writing the code or clicking the buttons. Right? It is putting the the stuff together. So we are at three forty three. What is Directus doing here? What is claw doing? Right? You can see it's creating fields for all the different relationships. It's adding those relationships. Now it's seeding the exercises. Yes. Let's create the default workout templates and create the flow to auto progress. So, again, this is this is a terrible way to build something for actual production in that, I've just got this set up in yellow mode, and, basically, I'm barely even reading these things. Yeah. We're I didn't even give you a chance to to comment on this. Right. But the the principles here are sound. Right? And that you've got a thinking partner in in something like Cloud Code or, you know, it could just be Cloud or Chad GPT. It doesn't really matter which AI you're using. It's It's helpful for thinking through these things. So if we take a look, right, now we've got our data model. We've got our exercise progress. We've got your different workouts with your different exercises already created here. Alright. You got your workout logs, which I'm assuming here's your workout. So Monday. Yeah. And that's the planning of it. Right? It's like yeah. Monday. Well, probably Oh, this is this is your yeah. The workout log. Workouts would be this is does this have your weight? No. It doesn't have the weights on it, does it? Yeah. So that part of it we're missing. Right? Maybe that is And I guess we aren't specific about that. Yeah. Well, there's weight at least in the logs. Yeah. So let's set up the Monday. Yesterday, Matt did this. Totally did it. Totally did it. I swear. Alright. So, you create your exercise log. You got a bench press, etcetera. Right? You completed five sets of five reps at two twenty five. Mhmm. Mhmm. Beast. Mhmm. Alright. So there you go. You got your exercise log. Let's check on what we're doing. Right? It is creating some operations now. So we should see, like, a flow in here. Should be creating these operations for it. Okay. We got a minute and twelve seconds left. I should have not said my sob story at the beginning. We'd have more time. But, I mean, this looks great. Right? Like, I think what's really interesting about it is that, you know, not even using Directus as, like, the interface for this, like, just slapping together a lovable front end or something that's much easier and accessible, like, from my mobile device than just, like, while I'm there, just blah blah blah. Yeah. Alright. So we're gonna go in, and we're gonna create this workout log again. It's Wednesday, pool day. Except you did it on Tuesday. It is we're gonna do hard incline. Oh, what's the pull? Lat pull downs. There you go. You had 20 sets. 20 reps. Crushed it. 110 pounds. I had my creosote right there. Let's see what happens. User exercise progress. Did we did we get an error on the flow? Oh, we got five seconds left. Know. Out of time. No. Run scripts. Cannot read properties. Okay. So, you know, Claude screwed up a bit on our run script somewhere in here where, you know, it's it's wrote some JavaScript code, which is nice. One of the nice things about the flows inside Directus is you can just write arbitrary JavaScript and run that. But there's a a few debugging issues. Right? So ten minutes in this case. And, you know, let's let's take a look. Right? Let's go to the tally. We've got got the workout exercises. We got the reps. We got your Monday, Wednesday, Friday, and we got the ability to log the workout. We got the rating the workouts. What we did not get is the automatic increase. So that logic, we did not get. And I would say that that So instead of the way it's interesting how I applied it to the full workout instead of the individual, like, exercises, which is probably why it got funky because it was probably waiting for us to put in all of the like, hey. Was this individual exercise hard or not? That's a good insight. Right? So it does show you I I I think I'm impressed with how far you can get into I mean that's right. Man, this is I mean, this is really the basis. You see a lot of apps out there like Fitbot or, like, Fitness AI, which are solid, but, you know, they're not customized to you. And being able to just, like, do this, get it custom, share it, you know, with whoever I'm working out with, it's powerful. Yeah. Another ten, fifteen minutes, we could have had, you know, nano banana generate images of these or we could have copied images. Like, diagrams of how how to do the exercises. Right? Yeah. Could create workout templates. Again, like, you could have, like, auto progression or auto calculation on there. And then even with the AI chat inside Directus. Right? Mhmm. Hey, man. Plan me a new workout. Right? Even if you let's say you you haven't built that, that front end yet Yeah. The native app that we're gonna build in ten minutes. Yo. You could still interact with this on your phone Yeah. Because it is all mobile responsive. Right? And I can open up the AI chat inside here, add an OpenAI API key, and, you know, I've got somebody that can log all my workouts or create new workouts for me as well right inside Directus. I don't have to code anything. Yeah. It's wild. Yeah. Well Well You know? I mean I we got far. We got farther than it would have taken me if I was gonna do this myself. Well, we'll give this, like, the the fair to Midland. Like, meh, like Yeah. It's okay. What is it? Gladiator? Don't know if it's up or down yet. I'll just give it this side. Don't know whether to kill it or let it live. There you go. Yeah. The alright. Yeah. Well, Matt, thanks for joining me. This is fun. Yeah. I do like, this is something that I I need myself personally, so I do kinda wanna keep working on it on the slide. Hey, man. This is fun. Great idea, man. Everybody's stealing my ideas lately. I'm just kidding. Give me access to this. I I wanna see you know what? We can also build a scoreboard so we can see, like, who can how much more I work with, bro. Bro, I used to say power I know. I I can't. I've seen you in person. There's no way. You're you got me beat. Short and squat, dude. See them. Alright. Cool. Well, thanks, man. Appreciate it. And, yeah, this is awesome. Yeah. Thoroughly enjoyed it. Alright. Everybody out there, that is it for this episode of one app ten minutes. Thanks for joining. Stay tuned for more episodes.","ba70a505-d112-4005-961a-306fd9c963e7",[723,724],"2d942561-61b4-4734-98e8-fb0093fc9799","54a3d149-4b85-4dcd-9a86-60157885f576",[],{"id":134,"number":135,"show":122,"year":136,"episodes":727},[138,139,140],{"id":140,"slug":729,"vimeo_id":730,"description":731,"tile":732,"length":733,"resources":8,"people":8,"episode_number":143,"published":701,"title":734,"video_transcript_html":735,"video_transcript_text":736,"content":8,"seo":737,"status":130,"episode_people":738,"recommendations":742,"season":743},"book-rating-app","1163987834","Bryant is joined by Vicky and Beth with the goal of building a book rating app in just 10 minutes using the MCP.","702ee809-e53c-43db-a8ee-6c63901ef22d",16,"Book Rating App","\u003Cp>Speaker 0: Welcome back to yet another episode of one app, ten minutes, your favorite show where we build apps, clones, whatever. Interesting stuff in ten minutes or less. Used to be I had an hour to do these, but you guys have low attention spans now. So we're down to ten minutes. Super excited for this episode.\u003C/p>\u003Cp>Very, very excited. We've got, a couple of rules before we dive in. Ten minutes to plan and build. So no more, no less. And then number two, the anti rule.\u003C/p>\u003Cp>Use whatever you have at your disposal. And I have got two amazing people from our team at Directus at at my disposal today, Beth and Vicky. Welcome to the show, guys.\u003C/p>\u003Cp>Speaker 1: Thanks for asking.\u003C/p>\u003Cp>Speaker 0: Yes. Yes. This is very exciting. Have you guys given any thought to what we're actually going to build today? I think You have a plan?\u003C/p>\u003Cp>We got a plan. Okay. That's good because I have a little idea what we're building, and that is, terrifying for me not knowing what we're actually going to build. So do you guys wanna lay it on me?\u003C/p>\u003Cp>Speaker 2: Yes. So we want to build a book rating app, because\u003C/p>\u003Cp>Speaker 1: in the\u003C/p>\u003Cp>Speaker 2: new year, we want to read more books.\u003C/p>\u003Cp>Speaker 0: And Book. Rating app. If I can ask, how did you guys come up with a a book rating app?\u003C/p>\u003Cp>Speaker 1: We're big on goals this year. So something Vicky and I have been talking about is, you know, just pull our goals out for the year. You gotta have a goal. You gotta have a purpose. You gotta have a track.\u003C/p>\u003Cp>You gotta gotta track your track your goals.\u003C/p>\u003Cp>Speaker 0: So is is book reading is, like, reading more books, like, near the top of the list? What what else is in consideration?\u003C/p>\u003Cp>Speaker 1: Oh, it's it's at the top of the list, for sure, for me at least. Vicky, have you got any other any other goals at the top of the list?\u003C/p>\u003Cp>Speaker 2: Lots of goals, lots of New Year's resolution, but this one's been trying to read more books for, like, the past ten years now. But\u003C/p>\u003Cp>Speaker 0: And today, we're gonna build the app that makes you read more books. I love it.\u003C/p>\u003Cp>Speaker 2: Keeps me accountable.\u003C/p>\u003Cp>Speaker 0: Okay. Alright. Well, you guys know how the show works. I'm going to start the timer. Basically, what we've got here is a totally blank instance of Directus.\u003C/p>\u003Cp>I've got, our Directus MCP connected so that we can, leverage some AI tools, but I'm gonna hit go. Are you guys ready?\u003C/p>\u003Cp>Speaker 1: Are you ready?\u003C/p>\u003Cp>Speaker 0: I I was born in. Sync. I know. That is, that's a good thing.\u003C/p>\u003Cp>Speaker 1: I'm jealous.\u003C/p>\u003Cp>Speaker 0: Alright. So when it comes to our book rating app, what kind of functionality do you guys wanna see out of this thing?\u003C/p>\u003Cp>Speaker 1: So we need some book information. So that would be title, genre, author\u003C/p>\u003Cp>Speaker 0: Okay.\u003C/p>\u003Cp>Speaker 1: And then the name of genre.\u003C/p>\u003Cp>Speaker 0: You guys are going too fast for me. Alright. We need books. We need genres. We need author, which clearly I can't spell.\u003C/p>\u003Cp>Probably a good thing we're gonna use AI. And then we like, are you guys are gonna share this app? Like, are you going to compete for who reads the most books?\u003C/p>\u003Cp>Speaker 1: Sure. It'd be the leaderboard. Would\u003C/p>\u003Cp>Speaker 0: be good. Writing, comments, notes. We wanna build a\u003C/p>\u003Cp>Speaker 1: Live it.\u003C/p>\u003Cp>Speaker 0: Book leaderboard. Leaderboard. Book leaderboard. Okay. Genre, author.\u003C/p>\u003Cp>Cool.\u003C/p>\u003Cp>Speaker 1: Maybe some notes. Yeah. You've got comments. Perfect.\u003C/p>\u003Cp>Speaker 0: Comments, notes. Notes is fine. We'll change that up. Alright. So, yeah, this is just the the functionality that we need.\u003C/p>\u003Cp>The first thing I'm gonna do is hop into Claude, and I've I've got this tool called Super Whisperer that I've been using that just transcribes my voice. We're pressed for time. So here we go. Hi, Claude. I want you to help us build a book rating app for my two amazing colleagues.\u003C/p>\u003Cp>They wanna read more books in the new year. I'm going to paste the outline of a Directus schema. I want you to fill that out, and then I want you to, plan with me quickly and then add it to our directus instance for us. Alright. So click a button.\u003C/p>\u003Cp>This thing transcribes everything for me with nice punctuation and no misspellings. You know, I've got these these sausage fingers I complain about all the time. Alright. So we could see that the MCP is now looking at the direct to schema, which is, should be blank in this case. And now Claude should oh, I gotta give it the actual outline, though, don't I?\u003C/p>\u003Cp>That makes sense. There we go.\u003C/p>\u003Cp>Speaker 1: It's very polite about it.\u003C/p>\u003Cp>Speaker 0: It is very polite. Yeah. I do I I'm not sure if if you're a person who who does the please and thank you, but, you know, I do as well. I'm like, hey. This could end in April.\u003C/p>\u003Cp>I don't I don't wanna be terminated. So Awesome. Alright. So we got a couple of questions, guys. Genre collections.\u003C/p>\u003Cp>Let's do a separate genre collection. I'll make that call for you. As far as the rating scale, do we wanna do one to five stars, one to ten, one to a 100?\u003C/p>\u003Cp>Speaker 1: One to five stars, please. One to five stars.\u003C/p>\u003Cp>Speaker 0: One to five stars. Ratings should track which user for sure. Leaderboard leaderboard. I'll set up. Do you guys care about cover image, publication year?\u003C/p>\u003Cp>Let's\u003C/p>\u003Cp>Speaker 1: give me to description\u003C/p>\u003Cp>Speaker 0: for the book. Cool. Alright. Go, Claude. Go.\u003C/p>\u003Cp>We've got, six minutes and\u003C/p>\u003Cp>Speaker 1: We're doing well on time. I\u003C/p>\u003Cp>Speaker 0: think we are. Yeah. Certainly. Ready. Go for it.\u003C/p>\u003Cp>Alright. So the native MCP has access to create collections, relationships, and also fields inside your direct assistance. So we could see here here, this is using Claude. Of course, if you're not using Claude code, which is, you know, more technical, you know, the terminal UI is maybe not the best for some of this stuff. If you're using cursor or chat GPT or just the cloud AI, you can do these same operations.\u003C/p>\u003Cp>Right? The other thing I'm doing here is just enabling yellow mode, which, Jonathan, if you're watching this one, please don't get mad at me. This is just a test instance. Never recommend that in production because, you know, any schema changes, you can lose your data. Alright.\u003C/p>\u003Cp>So what do we see? How are we looking, Beth? I think authors good. Book ratings, date created, the user. Cool.\u003C/p>\u003Cp>Books. Alright. Let's see if this actually works. Cool. Okay.\u003C/p>\u003Cp>What's the what's the first one\u003C/p>\u003Cp>Speaker 1: you're looking? Listening to an audiobook called Thinking in Bets.\u003C/p>\u003Cp>Speaker 0: Thinking in Bets. Does it\u003C/p>\u003Cp>Speaker 1: making smart decisions without all the data. It was actually, recommended by another one of our colleagues.\u003C/p>\u003Cp>Speaker 0: There we go. Alright. Who is the author?\u003C/p>\u003Cp>Speaker 1: I have my phone ready because I knew this was coming. Annie Duke.\u003C/p>\u003Cp>Speaker 0: Annie Duke? Annie Duke?\u003C/p>\u003Cp>Speaker 1: Yeah. Beautiful.\u003C/p>\u003Cp>Speaker 0: Alright. It is, I don't I don't know the genre, but, are you far enough along to are you far enough along to give it a rating?\u003C/p>\u003Cp>Speaker 1: So far, I like it. I think it's it's hovering around, like, like, a three or four. Let's see four for now. It starts off. I like it.\u003C/p>\u003Cp>I like it.\u003C/p>\u003Cp>Speaker 0: It's interesting. There we go. Alright. So there we go. We've got a book.\u003C/p>\u003Cp>We've got some book ratings. We've got an author. We've got a genre. We could see all that being created. Let's just hop in here and, like, take Claude, populate our data with some books and genres plus authors.\u003C/p>\u003Cp>So while it does that, we got three minutes and fifty six seconds. You know, I could certainly give you time. A plethora. That's a great way to describe it. Alright.\u003C/p>\u003Cp>Let's let's work on creating a dashboard so we could compare you guys. Right? Who is going to win. So book ratings, we we see the user there. I've it's locked to myself.\u003C/p>\u003Cp>Okay. I'm just gonna quickly change that to be not read only so I can change it. And I'm gonna create you guys as users real quick. Obviously, you're not gonna be able to log in, but I wanna have you guys in here. Alright.\u003C/p>\u003Cp>This is best book rating. Alright. Vicky, do you or what book are you reading?\u003C/p>\u003Cp>Speaker 2: Well, I watched the housemaid, the movie, but I realized it was a book too. So I'm starting to read that to compare to the movie itself. Sorry. I'm a movie first.\u003C/p>\u003Cp>Speaker 1: It's good because then you can, like, picture them.\u003C/p>\u003Cp>Speaker 0: Who is the who's the author?\u003C/p>\u003Cp>Speaker 2: The author is Freda McFadden. So f r e I d a, and then last name Freda\u003C/p>\u003Cp>Speaker 0: McFadden. Alright. Look at those typing skills. And that is I don't know what the genre is.\u003C/p>\u003Cp>Speaker 2: Like, thriller suspense.\u003C/p>\u003Cp>Speaker 0: Thriller.\u003C/p>\u003Cp>Speaker 1: Thriller is good.\u003C/p>\u003Cp>Speaker 0: Not to be confused with the Michael Jackson song. Okay. Alright. So you both have two ratings in here. Let's create the dashboard.\u003C/p>\u003Cp>Dashboard. We'll just call it dashboard, which bragging on my spelling, that is totally misspelled. Alright. So we are going to add a dashboard. Let's check-in on Claude.\u003C/p>\u003Cp>Go ahead. Create a lot. Fire away, my friend. Create some book ratings for the users, Beth and Vicky. Let's see if it'll do that while we're doing this.\u003C/p>\u003Cp>Alright. So we wanna see who has read the most books.\u003C/p>\u003Cp>Speaker 1: Beth.\u003C/p>\u003Cp>Speaker 0: Let's just do a simple metric, book rating. What is the field that we're gonna count? Let's just count the ID of the book. So we're gonna count distinct IDs, and we're gonna add a filter where the user first name contains Beth. Alright.\u003C/p>\u003Cp>This should give us that. We'll add a header. This is best books. Bath pick a color. 55.\u003C/p>\u003Cp>No pressure.\u003C/p>\u003Cp>Speaker 1: Orange. Orange. Orange. Orange. Thank you.\u003C/p>\u003Cp>Speaker 0: GraphQL validation error. Why does it matter like that? Field. Oh, I didn't select a field. Best books.\u003C/p>\u003Cp>Alright. We're gonna duplicate this. And now we got Vicky. Vicky, pick a color.\u003C/p>\u003Cp>Speaker 2: Green.\u003C/p>\u003Cp>Speaker 0: Green. Vicky's books. There you go. You got zero. Looks like Beth wins.\u003C/p>\u003Cp>Can't query the user's collection directly. Do we have some books in here? Let's alright. Quickly, five seconds left. Come on.\u003C/p>\u003Cp>Let's help Vicky out. Book pick a book. Oh, Vicky. Of course. There you go.\u003C/p>\u003Cp>Oh, no. The rating is required. Vicky likes it. It is a five. There you go.\u003C/p>\u003Cp>And boom. No. Refresh. Still doesn't like you. What happened?\u003C/p>\u003Cp>Speaker 1: I think you didn't assign the user to be away from\u003C/p>\u003Cp>Speaker 0: Yeah. It's been a preset me. So Yeah. It looks like I read more books than both of of you guys in this scenario. But alright.\u003C/p>\u003Cp>It will end on a one to one draw. This is the book rating app. You know, I think it would have been fun to maybe put together, like, a front end for this, but, the fact that we could put this together super quickly with you guys in in ten minutes, I think that's a win. Do we\u003C/p>\u003Cp>Speaker 1: Definitely a win. I feel like we should insert the win, little little frame. I think it would be really good if we had, you know, they had do, like, the top 100 books ever or, like, the best 500 books ever. You could so easily, like, include all of those books and just tick them off and rate it that way.\u003C/p>\u003Cp>Speaker 0: Is that what you guys are gonna do? Like No. Probably not.\u003C/p>\u003Cp>Speaker 1: I have I have my collection here and in the library a lot these days. So\u003C/p>\u003Cp>Speaker 0: That's very ambitious. 500 books this year.\u003C/p>\u003Cp>Speaker 1: Oh, no. Absolutely not. I I said 52 books this year, one a week.\u003C/p>\u003Cp>Speaker 0: 52 a week. 52 a week.\u003C/p>\u003Cp>Speaker 1: Really? I'm I'm I'm aiming for a 100, but I don't want to I don't want two a week. Because it's also, like, audiobooks and short books and poetry books. Less screen time for me is the aim. Vicky, have you got a number in mind?\u003C/p>\u003Cp>Speaker 2: I was trying to aim for 5 a year, but\u003C/p>\u003Cp>Speaker 1: it's so soft. It's good.\u003C/p>\u003Cp>Speaker 0: $5.05 a year. Yeah. I'd look if you're trying to hit 52, I've got a bunch of kids' books that I can like, my kids have exhausted. I can send them don't just use. You could you could blow through them, like, three, four, five a day.\u003C/p>\u003Cp>No no big deal. Beautiful.\u003C/p>\u003Cp>Speaker 2: There's no rules to that leaderboard, so I might as well take it.\u003C/p>\u003Cp>Speaker 0: Yeah. No validation. It's no no doctor Seuss. No no big deal. Anyway, alright, guys.\u003C/p>\u003Cp>Thank you so\u003C/p>\u003Cp>Speaker 1: much, Brian.\u003C/p>\u003Cp>Speaker 0: I've I've really enjoyed this. I hope you guys come back on. We can expand the book rating app. And the next next challenge we'll do is actually making you guys read the book somehow. So I I don't know how to build the software to do that yet, but next time.\u003C/p>\u003Cp>Speaker 1: We'll get we'll get AI to read it for us and take it to digest them and to feed them back to us. It'll be fine.\u003C/p>\u003Cp>Speaker 0: There you go. That's an idea. Alright. Thanks for coming on. Really enjoyed it, and stay tuned for the next episode of one app, ten minutes.\u003C/p>\u003Cp>Speaker 1: Bye. See you guys.\u003C/p>","Welcome back to yet another episode of one app, ten minutes, your favorite show where we build apps, clones, whatever. Interesting stuff in ten minutes or less. Used to be I had an hour to do these, but you guys have low attention spans now. So we're down to ten minutes. Super excited for this episode. Very, very excited. We've got, a couple of rules before we dive in. Ten minutes to plan and build. So no more, no less. And then number two, the anti rule. Use whatever you have at your disposal. And I have got two amazing people from our team at Directus at at my disposal today, Beth and Vicky. Welcome to the show, guys. Thanks for asking. Yes. Yes. This is very exciting. Have you guys given any thought to what we're actually going to build today? I think You have a plan? We got a plan. Okay. That's good because I have a little idea what we're building, and that is, terrifying for me not knowing what we're actually going to build. So do you guys wanna lay it on me? Yes. So we want to build a book rating app, because in the new year, we want to read more books. And Book. Rating app. If I can ask, how did you guys come up with a a book rating app? We're big on goals this year. So something Vicky and I have been talking about is, you know, just pull our goals out for the year. You gotta have a goal. You gotta have a purpose. You gotta have a track. You gotta gotta track your track your goals. So is is book reading is, like, reading more books, like, near the top of the list? What what else is in consideration? Oh, it's it's at the top of the list, for sure, for me at least. Vicky, have you got any other any other goals at the top of the list? Lots of goals, lots of New Year's resolution, but this one's been trying to read more books for, like, the past ten years now. But And today, we're gonna build the app that makes you read more books. I love it. Keeps me accountable. Okay. Alright. Well, you guys know how the show works. I'm going to start the timer. Basically, what we've got here is a totally blank instance of Directus. I've got, our Directus MCP connected so that we can, leverage some AI tools, but I'm gonna hit go. Are you guys ready? Are you ready? I I was born in. Sync. I know. That is, that's a good thing. I'm jealous. Alright. So when it comes to our book rating app, what kind of functionality do you guys wanna see out of this thing? So we need some book information. So that would be title, genre, author Okay. And then the name of genre. You guys are going too fast for me. Alright. We need books. We need genres. We need author, which clearly I can't spell. Probably a good thing we're gonna use AI. And then we like, are you guys are gonna share this app? Like, are you going to compete for who reads the most books? Sure. It'd be the leaderboard. Would be good. Writing, comments, notes. We wanna build a Live it. Book leaderboard. Leaderboard. Book leaderboard. Okay. Genre, author. Cool. Maybe some notes. Yeah. You've got comments. Perfect. Comments, notes. Notes is fine. We'll change that up. Alright. So, yeah, this is just the the functionality that we need. The first thing I'm gonna do is hop into Claude, and I've I've got this tool called Super Whisperer that I've been using that just transcribes my voice. We're pressed for time. So here we go. Hi, Claude. I want you to help us build a book rating app for my two amazing colleagues. They wanna read more books in the new year. I'm going to paste the outline of a Directus schema. I want you to fill that out, and then I want you to, plan with me quickly and then add it to our directus instance for us. Alright. So click a button. This thing transcribes everything for me with nice punctuation and no misspellings. You know, I've got these these sausage fingers I complain about all the time. Alright. So we could see that the MCP is now looking at the direct to schema, which is, should be blank in this case. And now Claude should oh, I gotta give it the actual outline, though, don't I? That makes sense. There we go. It's very polite about it. It is very polite. Yeah. I do I I'm not sure if if you're a person who who does the please and thank you, but, you know, I do as well. I'm like, hey. This could end in April. I don't I don't wanna be terminated. So Awesome. Alright. So we got a couple of questions, guys. Genre collections. Let's do a separate genre collection. I'll make that call for you. As far as the rating scale, do we wanna do one to five stars, one to ten, one to a 100? One to five stars, please. One to five stars. One to five stars. Ratings should track which user for sure. Leaderboard leaderboard. I'll set up. Do you guys care about cover image, publication year? Let's give me to description for the book. Cool. Alright. Go, Claude. Go. We've got, six minutes and We're doing well on time. I think we are. Yeah. Certainly. Ready. Go for it. Alright. So the native MCP has access to create collections, relationships, and also fields inside your direct assistance. So we could see here here, this is using Claude. Of course, if you're not using Claude code, which is, you know, more technical, you know, the terminal UI is maybe not the best for some of this stuff. If you're using cursor or chat GPT or just the cloud AI, you can do these same operations. Right? The other thing I'm doing here is just enabling yellow mode, which, Jonathan, if you're watching this one, please don't get mad at me. This is just a test instance. Never recommend that in production because, you know, any schema changes, you can lose your data. Alright. So what do we see? How are we looking, Beth? I think authors good. Book ratings, date created, the user. Cool. Books. Alright. Let's see if this actually works. Cool. Okay. What's the what's the first one you're looking? Listening to an audiobook called Thinking in Bets. Thinking in Bets. Does it making smart decisions without all the data. It was actually, recommended by another one of our colleagues. There we go. Alright. Who is the author? I have my phone ready because I knew this was coming. Annie Duke. Annie Duke? Annie Duke? Yeah. Beautiful. Alright. It is, I don't I don't know the genre, but, are you far enough along to are you far enough along to give it a rating? So far, I like it. I think it's it's hovering around, like, like, a three or four. Let's see four for now. It starts off. I like it. I like it. It's interesting. There we go. Alright. So there we go. We've got a book. We've got some book ratings. We've got an author. We've got a genre. We could see all that being created. Let's just hop in here and, like, take Claude, populate our data with some books and genres plus authors. So while it does that, we got three minutes and fifty six seconds. You know, I could certainly give you time. A plethora. That's a great way to describe it. Alright. Let's let's work on creating a dashboard so we could compare you guys. Right? Who is going to win. So book ratings, we we see the user there. I've it's locked to myself. Okay. I'm just gonna quickly change that to be not read only so I can change it. And I'm gonna create you guys as users real quick. Obviously, you're not gonna be able to log in, but I wanna have you guys in here. Alright. This is best book rating. Alright. Vicky, do you or what book are you reading? Well, I watched the housemaid, the movie, but I realized it was a book too. So I'm starting to read that to compare to the movie itself. Sorry. I'm a movie first. It's good because then you can, like, picture them. Who is the who's the author? The author is Freda McFadden. So f r e I d a, and then last name Freda McFadden. Alright. Look at those typing skills. And that is I don't know what the genre is. Like, thriller suspense. Thriller. Thriller is good. Not to be confused with the Michael Jackson song. Okay. Alright. So you both have two ratings in here. Let's create the dashboard. Dashboard. We'll just call it dashboard, which bragging on my spelling, that is totally misspelled. Alright. So we are going to add a dashboard. Let's check-in on Claude. Go ahead. Create a lot. Fire away, my friend. Create some book ratings for the users, Beth and Vicky. Let's see if it'll do that while we're doing this. Alright. So we wanna see who has read the most books. Beth. Let's just do a simple metric, book rating. What is the field that we're gonna count? Let's just count the ID of the book. So we're gonna count distinct IDs, and we're gonna add a filter where the user first name contains Beth. Alright. This should give us that. We'll add a header. This is best books. Bath pick a color. 55. No pressure. Orange. Orange. Orange. Orange. Thank you. GraphQL validation error. Why does it matter like that? Field. Oh, I didn't select a field. Best books. Alright. We're gonna duplicate this. And now we got Vicky. Vicky, pick a color. Green. Green. Vicky's books. There you go. You got zero. Looks like Beth wins. Can't query the user's collection directly. Do we have some books in here? Let's alright. Quickly, five seconds left. Come on. Let's help Vicky out. Book pick a book. Oh, Vicky. Of course. There you go. Oh, no. The rating is required. Vicky likes it. It is a five. There you go. And boom. No. Refresh. Still doesn't like you. What happened? I think you didn't assign the user to be away from Yeah. It's been a preset me. So Yeah. It looks like I read more books than both of of you guys in this scenario. But alright. It will end on a one to one draw. This is the book rating app. You know, I think it would have been fun to maybe put together, like, a front end for this, but, the fact that we could put this together super quickly with you guys in in ten minutes, I think that's a win. Do we Definitely a win. I feel like we should insert the win, little little frame. I think it would be really good if we had, you know, they had do, like, the top 100 books ever or, like, the best 500 books ever. You could so easily, like, include all of those books and just tick them off and rate it that way. Is that what you guys are gonna do? Like No. Probably not. I have I have my collection here and in the library a lot these days. So That's very ambitious. 500 books this year. Oh, no. Absolutely not. I I said 52 books this year, one a week. 52 a week. 52 a week. Really? I'm I'm I'm aiming for a 100, but I don't want to I don't want two a week. Because it's also, like, audiobooks and short books and poetry books. Less screen time for me is the aim. Vicky, have you got a number in mind? I was trying to aim for 5 a year, but it's so soft. It's good. $5.05 a year. Yeah. I'd look if you're trying to hit 52, I've got a bunch of kids' books that I can like, my kids have exhausted. I can send them don't just use. You could you could blow through them, like, three, four, five a day. No no big deal. Beautiful. There's no rules to that leaderboard, so I might as well take it. Yeah. No validation. It's no no doctor Seuss. No no big deal. Anyway, alright, guys. Thank you so much, Brian. I've I've really enjoyed this. I hope you guys come back on. We can expand the book rating app. And the next next challenge we'll do is actually making you guys read the book somehow. So I I don't know how to build the software to do that yet, but next time. We'll get we'll get AI to read it for us and take it to digest them and to feed them back to us. It'll be fine. There you go. That's an idea. Alright. Thanks for coming on. Really enjoyed it, and stay tuned for the next episode of one app, ten minutes. Bye. See you guys.","9e7335b9-89fb-4faf-9cbe-11d2216e51f0",[739,740,741],"db77dc81-834c-4fbe-99b4-c17d40fc7bf1","2c87cbd3-9b86-4473-ad16-859d4dc73cb4","9f92db51-afe2-421f-847e-8a71cc526ce8",[],{"id":134,"number":135,"show":122,"year":136,"episodes":744},[138,139,140],{"reps":746},[747,803],{"name":748,"sdr":8,"link":749,"countries":750,"states":752},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[751],"United States",[753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802],"Michigan","Indiana","Ohio","West Virginia","Kentucky","Virginia","Tennessee","North Carolina","South Carolina","Georgia","Florida","Alabama","Mississippi","New York","MI","IN","OH","WV","KY","VA","TN","NC","SC","GA","FL","AL","MS","NY","Connecticut","CT","Delaware","DE","Maine","ME","Maryland","MD","Massachusetts","MA","New Hampshire","NH","New Jersey","NJ","Pennsylvania","PA","Rhode Island","RI","Vermont","VT","Washington DC","DC",{"name":804,"link":805,"countries":806},"Michelle Riber","https://meetings.hubspot.com/mriber",[807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,784,995,996],"Albania","ALB","Algeria","DZA","Andorra","AND","Angola","AGO","Austria","AUT","Belgium","BEL","Benin","BEN","Bosnia and Herzegovina","BIH","Botswana","BWA","Bulgaria","BGR","Burkina Faso","BFA","Burundi","BDI","Cameroon","CMR","Cape Verde","CPV","Central African Republic","CAF","Chad","TCD","Comoros","COM","Côte d'Ivoire","CIV","Croatia","HRV","Czech Republic","CZE","Democratic Republic of Congo","COD","Denmark","DNK","Djibouti","DJI","Egypt","EGY","Equatorial Guinea","GNQ","Eritrea","ERI","Estonia","EST","Eswatini","SWZ","Ethiopia","ETH","Finland","FIN","France","FRA","Gabon","GAB","Gambia","GMB","Ghana","GHA","Greece","GRC","Guinea","GIN","Guinea-Bissau","GNB","Hungary","HUN","Iceland","ISL","Ireland","IRL","Italy","ITA","Kenya","KEN","Latvia","LVA","Lesotho","LSO","Liberia","LBR","Libya","LBY","Liechtenstein","LIE","Lithuania","LTU","Luxembourg","LUX","Madagascar","MDG","Malawi","MWI","Mali","MLI","Malta","MLT","Mauritania","MRT","Mauritius","MUS","Moldova","MDA","Monaco","MCO","Montenegro","MNE","Morocco","MAR","Mozambique","MOZ","Namibia","NAM","Niger","NER","Nigeria","NGA","North Macedonia","MKD","Norway","NOR","Poland","POL","Portugal","PRT","Republic of Congo","COG","Romania","ROU","Rwanda","RWA","San Marino","SMR","São Tomé and Príncipe","STP","Senegal","SEN","Serbia","SRB","Seychelles","SYC","Sierra Leone","SLE","Slovakia","SVK","Slovenia","SVN","Somalia","SOM","South Africa","ZAF","South Sudan","SSD","Spain","ESP","Sudan","SDN","Sweden","SWE","Tanzania","TZA","Togo","TGO","Tunisia","TUN","Uganda","UGA","United Kingdom","GBR","Vatican City","VAT","Zambia","ZMB","Zimbabwe","ZWE","UK","Germany","Netherlands","Switzerland","CH","NL",1773850419639]