[{"data":1,"prerenderedAt":437},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-databox-clone":121,"100-apps-100-hours-databox-clone-next":168,"sales-reps":185},{"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,"slug":123,"vimeo_id":124,"description":125,"tile":126,"length":127,"resources":8,"people":8,"episode_number":128,"published":129,"title":130,"video_transcript_html":131,"video_transcript_text":132,"content":8,"status":133,"episode_people":134,"recommendations":149,"season":150,"seo":167},"620cf225-a23a-415a-ad95-9ba8e2dec984","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",63,7,"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.","published",[135],{"people_id":136},{"id":137,"first_name":138,"last_name":139,"avatar":140,"bio":141,"links":142},"791e1503-1d88-463d-9347-0b9192933576","Bryant","Gillespie","9013afc8-e8d7-4182-9b18-44db08117bb9","Developer Advocate at Directus",[143,146],{"url":144,"service":145},"https://directus.io/team/bryant-gillespie","website",{"service":147,"url":148},"github","https://github.com/bryantgillespie",[],{"id":151,"number":152,"year":153,"episodes":154,"show":164},"14fda5f2-95de-4dbe-a4e2-3fd956c21c19",2,"2024",[155,156,157,158,159,160,122,161,162,163],"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","b8b36125-7a4a-40e4-85f6-f4fe9138085e","385bdd7d-038d-4f9c-8037-357e5272420a","383c24d5-b6b5-4d66-aba6-6997af5f77b4",{"title":165,"tile":166},"100 Apps In 100 Hours","fb0f9d45-be21-4634-94d4-2ef1cc5146f2",{"title":8,"meta_description":8},{"id":169,"slug":170,"season":171,"vimeo_id":172,"description":173,"tile":174,"length":175,"resources":8,"people":8,"episode_number":176,"published":177,"title":178,"video_transcript_html":179,"video_transcript_text":180,"content":8,"seo":181,"status":133,"episode_people":182,"recommendations":184},"ec88bef1-fffd-43eb-9d93-3123dc381b97","ai-letters-to-santa","d6b229fe-38fc-495b-ba0c-c574ebfea38f","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,1,"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",[183],"cc9262db-1ae2-4bc1-a1f9-7d2fc51a396d",[],{"reps":186},[187,243],{"name":188,"sdr":8,"link":189,"countries":190,"states":192},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[191],"United States",[193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242],"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":244,"link":245,"countries":246},"Michelle Riber","https://meetings.hubspot.com/mriber",[247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,224,435,436],"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",1773850448194]