[{"data":1,"prerenderedAt":439},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-lms":121,"100-apps-100-hours-lms-next":171,"sales-reps":187},{"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":128,"episode_number":132,"published":133,"title":134,"video_transcript_html":135,"video_transcript_text":136,"content":8,"status":137,"episode_people":138,"recommendations":152,"season":153,"seo":8},"8434838a-8e4f-489a-8da2-fbf10de5de6a","lms","895940893","From creating and organizing course content, managing instructors and students, and tracking enrollments and completions, Bryant has sixty minutes on the clock.","73ceb5b3-6a79-484f-8254-a4d41ab970b3",64,[129],{"name":130,"url":131},"Bryant Gillespie","https://directus.io/team/bryant-gillespie",2,"2023-12-22","Mission: Learning Management System","\u003Cp>Speaker 0: Welcome back to the next episode of 100 apps, 100 hours, where we try to recreate and build your favorite apps in 1 hour or less, or die trying. Hopefully not dying. I'm your host Brian Gillespie, developer advocate at Directus. Super nice to have you. Today we've got a app that is near and dear to my heart.\u003C/p>\u003Cp>I've created a lot of courses in the past, so we are going to be building a learning management system, an LMS. There's a few examples that I took a look at in the the research for this prior to. I've got my proper dad attire on and we are going to dive in and build some apps. So let's just add a little bit of context for you guys before we start the clock. If you take a look at sites like Kajabe or Podia, these are online learning management systems.\u003C/p>\u003Cp>Most of these are software as a service where you can sign up for, $159 a month, it looks like, or $199 a month and sell your courses. The interface for these, if we can maybe navigate to one of, Podia's websites here. We've got like a main navigation on the left. We've got, you know, the courses that show up on the right. If we take a look at Udemy, that's a kind of a different case where you're on their platform, you're not white labeling, but you can create your own courses and load those up and sell those to their specific audience.\u003C/p>\u003Cp>So I think we'll land somewhere in between all of these different tools today. Should be very fun, very interesting. So with that, let's, let's dive into this. I'm gonna start my little mouse pose thing so I can highlight things on screen as we go through and build. So, I want to kind of show you what I am working with just so there is no, you're aware, there's no cheating behind the scenes here.\u003C/p>\u003Cp>I do have a Directus instance already started. I haven't logged in yet, but that is a blank Instance ready to go. That's what we'll use for the back end of the app. We also have a Nuxt 3 starter application. I do have like a Directus SDK already preconfigured and a couple of utilities just to make building a little bit easier.\u003C/p>\u003Cp>But aside from that we can see this is just a blank slate. There's a login form, text component and an upload component, just to make this a little easier. A lot of this is boilerplate functionality, I don't want to bore you guys with that. So that's the setup. Alright.\u003C/p>\u003Cp>Let's dive into building the app and we will start the clock on this. So we now have 60 minutes to develop this out, But before we actually do any code, I want to like sketch this out. And I I love using Figma for this. So the first thing we want to flesh out is our data model. What's our schema?\u003C/p>\u003Cp>We're going to have courses. So within each course, we have some lessons. We probably have some modules. Alright. Maybe these are not huge per se.\u003C/p>\u003Cp>We'll just make them smaller. What else are we gonna have? We're gonna have users. We'll probably have some instructors. Right?\u003C/p>\u003Cp>We want to be able to track the instructors on the course. This is probably a a pretty good starting point. Now as far as the functionality that we need, so let's just call this our data model. Cool. We'll go over here.\u003C/p>\u003Cp>Inspect this out. Functionality. Alright. Let's go through and flesh this out a little bit. We want to view a list of courses, view an individual course, and this is gonna be on the front end.\u003C/p>\u003Cp>View individual lessons, we probably need to sign up for our course. We just walk it through the entire process. View individual lessons. We want some authentication, so we wanna be able to log in. And, what else?\u003C/p>\u003Cp>We wanna be able to track progress. Right? And, to that end we probably need like a collection to track enrollments. So who are our users, what courses are they enrolled in, what is the completion percentage for those. Alright.\u003C/p>\u003Cp>So this seems like a pretty good start. We'll continually come back and refer to this and refine it as we go through. But the first thing that I wanna do is start into our back end. So I'm gonna pull up Directus in this case, and let's see if we can zoom in just a little bit to make it easier for everybody. Now I've got this running on Docker locally, but the easiest way to run Directus is using Directus Cloud.\u003C/p>\u003Cp>And I need to make sure I've got the correct password here. Alright. So we've got a blank Directus instance. We're going to flesh out this data model in no time at all. So let's create our 1st collection, we'll call it Courses, and we will use the generated UUID as the primary key field.\u003C/p>\u003Cp>We'll just go in and for now we'll just select all of these defaults for tracking date created, user created, etcetera, handy to have. And for our courses we probably want a name for the course, that's great. We want to have a description for the course, so that will be a WYSIWYG editor. That way we could support HTML and rich text formatting. What else do we need as as far as our course data?\u003C/p>\u003Cp>We've probably got an image for the course as well. So we could call this image. We could have multiple images, but let's just stick with a single featured image, and we'll use that. Great. So now if we back up, we've got the start to our courses data model.\u003C/p>\u003Cp>Let's go in and flesh out the other parts of our course. So each course will be made up of several different modules. So the modules, again, we can add all these fields. We may not need those. I typically just add these because it has some preset functionality.\u003C/p>\u003Cp>Obviously the user created it will store the UUID for that particular user, updated date and times, those are all handy to have. So the modules, we will have a name for the module, and, you know, we could have like a short description for the module. Let's just stick with name, we'll keep it really simple. The other thing that we may want to have on the courses, this just reminds me, is maybe something like a slug. So just a pretty URL for that specific course.\u003C/p>\u003Cp>So we'll go in and set that up. One of the other things I could do really quickly in Directus is make sure this is URL safe. So there's just a, a field to set that up for us to automatically format that. Next, let's go in and add our lessons table, or lessons collection. We'll do the same, so what's the status, is this published or is it draft?\u003C/p>\u003Cp>And now in the lessons we'll have a name for the lesson. A lot of names here. We'll have the, let's call it Lesson Content, and that could be rich text. And because these are primarily videos, let's do a video URL. We can host those on Vimeo, YouTube, Bloom, however you wanted to do that.\u003C/p>\u003Cp>You could also just actually upload these into Directus as well. Alright, so now we've got kind of this side of the equation figured out. Directus already gives us users out of the box, which is great. We can go in and add these other collections. We'll have an instructor's collection.\u003C/p>\u003Cp>Instructor's, I hope that's how you spell it. So our course instructors, that will be we'll have a name for them. Let's have a bio, so that'll be a WYSIWYG field. And one of the things that I love most about Directus is just how easy it is to map out this data model and get a complete back end with an AI or API ready to query for my front end. So instead of working with dummy data, I can actually prototype live with real data.\u003C/p>\u003Cp>So we got our name, we got our bio, let's do an image, or we could call it avatar. Great. Cool. And last but not least, we've got enrollments. So we'll just, create all these separate tables, all these different collections within Directus.\u003C/p>\u003Cp>And now we can go back and flesh out the relationships between these, because that's where the tricky part comes in. Or not necessarily tricky with Directus, but, we wanna make sure that data model works correctly on the front end. So the when we start mapping these out we have our courses. Each course could have multiple modules, and each module could have multiple lessons. So those are one too many relationships inside Directus.\u003C/p>\u003Cp>So if we start fleshing this out, we'll go into the Courses collection and we'll use the relational fields inside Directus. So behind the scenes Directus will set up these relationships inside your SQL database and make sure everything plays nice with each other. So we're going to call this modules. So that's going to be our key. The related collection here is going to be modules.\u003C/p>\u003Cp>And then the foreign key, so our key inside the modules collection is going to be Course. We could choose whether we want a list or table layout, that's fine, and we'll just use the name of the module and we'll show a link to that module. So now our modules are set up and if I were to go to the actual module section we can see we've got the reverse relationship back to the course. And maybe we drag and drop this, give these some icons to make them look nice. So we've got a Course, maybe that is a folder looking object.\u003C/p>\u003Cp>It's great. Let's use black as the color. So we'll customize this a bit and make it look nice. The module, you know, let's look for a list, maybe. Looks great.\u003C/p>\u003Cp>And now let's add a lesson, we'll just use like a video icon for that. Perfect. Okay. So now if we go back to our modules, again we're going to create a one to many relationship, but this time we're going to do that on our lessons. So the key here will be called lessons and the related collection will be lessons.\u003C/p>\u003Cp>Our foreign key will be module. So just the singular because each lesson can only be in a single module. And we'll use the name of that lesson. Great. We'll show a link to the item.\u003C/p>\u003Cp>And now we've got that structure set up. Great. How do the other items relate? Right? We've got instructors.\u003C/p>\u003Cp>Instructors could have many different courses inside our platform, and a course could have multiple instructors. So that's a good use case for the many to many relationships inside Directus. And what we'll do is go to either courses or instructors, doesn't really matter which one. And instead of a one to many or a many to 1, we're going to use the many to many relationship. So here we'll call this instructors, that's gonna be our key.\u003C/p>\u003Cp>For our related collection, we're gonna use instructors. And we don't want to allow duplicates, we want to show a link to the item. If I wanted more control over what Directus creates, or uses for the name of the junction tables, I can go into the advanced field mode. So we can see in our junction collection Directus is going to create this inside our SQL database. Courses underscore instructors, and there's gonna be a Courses ID and an Instructor's ID.\u003C/p>\u003Cp>I can also add this relationship to the instructors field and we'll give this a sort just in case we want to have a head instructor and, you know, prioritize who is the primary instructor on the course. So we'll hit save, Directus will do some magic behind the scenes, and if I go in now I will see a hidden collection in our data model for courses underscore instructors. Savvy? Everything's looking good so far? Great.\u003C/p>\u003Cp>Alright, now we also want to flesh out our enrollments. So if we think of enrollments, enrollments are going to be the users that are enrolled in the individual courses and this setup will be a little bit different in that we go to Enrollments, there's gonna be a many to one relationship. So each enrollment can only have one course. So what are the course that this person enrolled in? Great.\u003C/p>\u003Cp>And then we're also going to have a many to one relationship with the Directus user. In this case we could just call it user as well. But the related collection here is going to be the Directus underscore users. That's our system collection for users within directus that, gives us all the authentication and user access and permissions. We could also keep track of progress or lessons completed, but for now let's just roll with this.\u003C/p>\u003Cp>Okay, a little OCD over things like icons and colors, so I will add some icons and colors here. We've got instructors. These will be people. Let's see what we've got. Nice friendly guy waving, that's perfect.\u003C/p>\u003Cp>Let's make that purple as well. Alright, so now if we look at our data model, this is looking pretty good. We will go into the front end and we can see that we've got courses, we've got modules, we've got lessons. Let's just create a course. Let's do published.\u003C/p>\u003Cp>And we'll call this 100 Apps in 100 hours, great. Watch to follow along as we build an LMS. While building an LMS? I don't know. It's kinda meta.\u003C/p>\u003Cp>No worries. Alright so we can, let's go to Unsplash and find a nice looking image. One of the nice things about Directus, and you'll hear me say that multiple times throughout this series, is the ability to import from a URL. So I can just copy that URL there, import that image, it will store it in my file library for me. And we'll give this a slug.\u003C/p>\u003Cp>So you'll see if I press space bar it will automatically format this for me so that it becomes URL safe. And let's go in and create a module. So we'll call this 1st module. We'll create a second module. Great.\u003C/p>\u003Cp>And we'll add an instructor. The dude building stuff and videos. Cool. I've got let's upload a photo. Great.\u003C/p>\u003Cp>Good. Solid. Okay. So now we've got our first course. Since we've set up these collections I could start querying this on the front end using the Directus APIs.\u003C/p>\u003Cp>So So if I just open this up in a new tab, I'm going to change the URL a little bit. We'll go to items/courses and boom, we get this permission access error. So I haven't enabled permissions. We've created this actual, collection. We've created several of these collections, but for the general public all these collections are access is forbidden by default.\u003C/p>\u003Cp>So I could open this up and you know, just allow public access for now to all of these. Once we went to production, obviously we want to restrict this per user. But if I just refresh you can see over here on the left, excuse me, we've got our data coming from the LMS, or the back end of our LMS, our direct us instance. Great. So we are about 15 minutes in.\u003C/p>\u003Cp>We've got our first bit of data modeling done. Let's dive into kind of the front end and start building, right? We will, I'm just gonna move this out of the way for now, and we'll get to a blank slate inside our code editor. We've got our direct assistance up here, but let's pull this up. Now within the pages directory of my Nuxt application, maybe we create a new folder for courses.\u003C/p>\u003Cp>This will give us a route slash courses on the front end. And we'll have an index route. That's great. We'll set up a default view component here and now we can start querying that data from Directus. One of the things that I do have in my little Directus module is a composable to actually query the front end of or query the Directus back end.\u003C/p>\u003Cp>So we can do something like this where we say const courses equals useDirectus. And this is using the Directus SDK on the the back end. We're gonna read items, courses, And I could go in and add some options here as well. You know, Copilot is showing me like a filter for published. But for now, because we haven't published anything, let's just take a look at this.\u003C/p>\u003Cp>We'll, log this out to the front end. And if we navigate to /courses, it looks like we have an object promise, so we wanna do await use directus, and boom, now we've got our data on the front end. Cool. So now we probably want to do a little bit of formatting, you know, add this, add maybe a card for this. One of the things that I am using or I've got set up in this Nuxt instance or this Nuxt template app is the Nuxt UI library, which is just a UI library put out by the Nuxt team.\u003C/p>\u003Cp>It helps you build apps that look great faster. It works perfectly for stuff like this. On a lot of other projects, I may use, you know, my own custom coded components. But for now let's do a card. We'll do v4 courses and courses because that is an array, course ID.\u003C/p>\u003Cp>Let's actually just do something like this where we've got v text. I do also have a text component, we'll call it course. Name. And we can use the Nuxt image component, course. Image.\u003C/p>\u003Cp>See how far that gets us. Okay. So we've got a card. We probably want to give a little bit of styling to the container. What is the I guess the muxu container It constrains the width.\u003C/p>\u003Cp>Okay. And then maybe we've got let's just go back. We'll do the keep this really simple. I also, use tailwind a lot. We'll do like a max width of 4 XL for now.\u003C/p>\u003Cp>Make this in x auto. And maybe we set these up in a grid with 3 columns on, like a large size. Okay. So we've got this actual course card. We're not rendering the actual image, which, is concerning me.\u003C/p>\u003Cp>You don't have permission to access this. So we've hit our first kind of snag here. Somehow I've managed to log myself out of Directus as well. So let's log back in. Directus.\u003C/p>\u003Cp>Alright. And now we will go in and we forgot to set permissions for our system collection. So all the Directus files, we're just going to make those public for now so we can take a look at that file. I could now see that and it should show up within our card as well. Great.\u003C/p>\u003Cp>You know, maybe we make this font bold, 2 x l. Make it a bit bigger. Let's make it giant, right, 4 x l. I need to take a look at my Vtex component actually. I think I've already got sizes on here, so we could just use the size prop instead.\u003C/p>\u003Cp>2XL. Great. Okay. Alright. So now we've got kind of an index page.\u003C/p>\u003Cp>This is a course listing. Let's wrap this with, another div. I'll go in and paste that. So now we've got a course listing, we've got a course card, How do we get to this actual course detail? So I will go in and, now we'll go into our courses folder within the pages directory And let's create actually let's create a new folder.\u003C/p>\u003Cp>And we'll use the brackets for this. This will give us a dynamic route. And we will use like slug or I could call this course or ID. We're using slug as the property for the course on the directive side, if I remember correctly. So we've got a slug for each of the courses.\u003C/p>\u003Cp>That's what we'll query by. Sounds great. And we will go in and now we will add like an index page for this course. So again, we'll build a detailed page here, just a simple index page, and let's do course, wait, use direct us, read item. We wanna read a single item.\u003C/p>\u003Cp>This is using our SDK context. And we'll use route params slug. That's not gonna cut it. Actually, we want to read multiple items. And the reason why is because we're giving each course an ID, a a UUID.\u003C/p>\u003Cp>And the Directus SDK, when you read a single item, you can look up by that primary key, in this case the ID field. But where we want to look up by the slug, we're going to use read items and then we're going to construct a filter. So it'll be like this where slug is equal to route dot params.slug. We're gonna use route. So we'll pick up the route from view.\u003C/p>\u003Cp>We use route dot params.slug. And again, this is why I love building this way with the back end first because I can use actual data to flesh this out. So now on our course listing page we want to add a link to that. So if we go back to the index page, we can wrap, both of these in a Nuxt link component. And for the to prop, let's just add, courses slash course ID or no, course dot slug.\u003C/p>\u003Cp>Alright, so now we should get a clickable link. We can see at the bottom of the screen it says 100 Apps, 100 Hours. And when I click on it it will give me, an array of the data in this case. But I would rather have just a, I'd rather have the actual image. So let's do something like this where constant course equals courses dot I or core the first item of the array.\u003C/p>\u003Cp>Maybe this could be a computer prop as well just in case that changes. Courses dot value cannot read properties of undefined. What is going on here? Let's back up and let's just keep it simple. Right.\u003C/p>\u003Cp>Course equals courses. Okay, there we go. So we've got our course, and now we can start fleshing this out. So if I go in and I look at the Nuxt UI Library, we've got some components here, we've got like a skeleton. We've got a container.\u003C/p>\u003Cp>We've got a card. What do some of these other apps look like? How do we actually get to their courses? Not a lot of great examples on the website. If we look at Udemy, we could see we've got like a course name, we've got a preview of the course, we've got, some of the lessons here.\u003C/p>\u003Cp>And then when you get into the actual, enrollment of that specific course, they've got like a kind of a sidebar layout. So let's create a new section. We'll do the course title, course description, follow along as we build. Okay. 100 apps, 100 hours.\u003C/p>\u003Cp>So the course description, we're gonna use v html for that. Course dot description. Okay. And now we've got that. We could apply some nice styling for this.\u003C/p>\u003Cp>Let's do the font. Black text 4 x l. We'll do a, let's try that U container again. And this is coming from the Nuxt UI library. Alright.\u003C/p>\u003Cp>So we got that. We've got, the description. Maybe we wanna add like a small bit of text to label that. This description class text extra small, text gray 500, something like that. And we can space these out a bit.\u003C/p>\u003Cp>So again, this is a bit crude. We're going for speed over beauty in this case. But we want to flesh out this functionality. Where did you go? Okay, so we got our course name, we got a description.\u003C/p>\u003Cp>Let's show our instructors. Alright. So this is gonna be our pclasstext. Let's do instructors. Okay.\u003C/p>\u003Cp>So we've got that. We'll add a bit of margin to it. Space it apart. We'll pull up the instructors. How are we doing on time?\u003C/p>\u003Cp>Quick time check. Got 30 minutes remaining to flesh this out. Gonna be a definite challenge here. So we've got the instructors. We'll do, should be course dot instructors, right?\u003C/p>\u003Cp>Let's just log that out and see what we get. Course dot instructors. So you can see Directus is giving me back an array of the different ID's for those instructors. I've said this multiple times, but this is one of the reasons why I really love Directus. Because I can go in and for, this is using the REST API.\u003C/p>\u003Cp>I can actually go in and do something like this where I say, hey, I want all the root level fields. And then for instructors, I want, I want a list of all the root level fields for the instructor. So I could get all the related fields in a single API call. Now where this is a many to many relationship, you can see I need to drill in one more level. So I have to go in and do one more level where I have instructors underscore ID and grab all those fields.\u003C/p>\u003Cp>So now I can see the instructor and we can set up like a, just a v four loop here. V four instructor in course dot instructors. And it looks like Copilot has added a lot of code that I did not mean to add here. So let's just pull that back. Be careful how many times you tab, right?\u003C/p>\u003Cp>So now let's take a look at instructor within that loop and see what we have. Are we even getting anything? Alright. We've got instructors. We've got the instructor ID.\u003C/p>\u003Cp>K. So we could do something like this. We could actually destructure that. Or can we? Instructor instructor's ID.\u003C/p>\u003Cp>And let's see what we get back with that. We're breaking stuff. Okay. Yeah. So we'll just run with this for now.\u003C/p>\u003Cp>Alright. So we've got our instructor, we've got an instructor ID. So we'll have instructor dot instructorsid.name. Let's wrap that P TECH. We'll do, Nuxt Image.\u003C/p>\u003Cp>And let's do, yeah. Maybe width 24 sounds good. 24 height, rounded full to make this a circle. And then the source is gonna be instructor dot instructorsid.avatar. And then we can do a divvhtml for the bio equals instructor dot instructorsid.bio.\u003C/p>\u003Cp>Okay. Cool. So now we've got a few things going on here. Let's actually look at the lessons. Right?\u003C/p>\u003Cp>We'll go into our Directus instance. If we go back to our specific course here, we didn't add any lessons to this specific module. So I'm just gonna pull up the Directus YouTube account. Our YouTube studio here will get to the Directus channel and take a look at our content. So I'm just gonna copy a few of these down.\u003C/p>\u003Cp>We're gonna add a couple of lessons within each one of these. Just gonna actually take these wholesale from the YouTube channel. Here's some content. Great. Alright.\u003C/p>\u003Cp>How do we do, we'll copy some of this. How to manage different versions. Blah blah blah, copy that video URL. Oh, that's actually a short. Not sure what that'll do.\u003C/p>\u003Cp>Let's, let's take an actual video. Great. So we've got our first module. Let's go into the second module and we will add one of my videos. Getting started with Agency OS.\u003C/p>\u003Cp>My wife tends to make fun of, my sausage fingers all the time. So lots of typos here. So we'll go in. Let's just copy one more of these so we've got something to look at on the front end. Project templates.\u003C/p>\u003Cp>Great. Okay. So So save this. Now we've got our course, we go back to our course detail page in this instance and, you know, let's add a new section over here where we've got our section. Looks like it get hub copilot for the win.\u003C/p>\u003Cp>We've got our lessons. Give that some margin. Give some space. And, so if we log each lesson, let's just take a look, we'll probably get the same, let's just close this out real quick. We'll do course dot lessons just to see what we're getting back from the API, which it doesn't look like anything, right?\u003C/p>\u003Cp>So if I open up the dev tools, we take a look at the network request, we dive into the data, the lessons are actually going to come through the different modules. Alright, so again, we can go back up to our fields section and we could do something like this where we go to modules, then we, drill down again into, our lessons. And we want to grab the module name. And within our lessons, again, we could just grab all the root level fields within that. And now we can see that second API call that was made.\u003C/p>\u003Cp>We've got our modules. We've got our lessons within that. And again, I only have to make a single API call. You know, we can certainly cache this if this data doesn't change, but it keeps the entire app very snappy and prevents me from having to make 35 different calls to get all the related data. Also kind of it follows a very GraphQL like structure as well where I can request just the data that I need.\u003C/p>\u003Cp>I don't necessarily have to go in and use these wildcards to grab those root level fields. I could go in and say something like name, description, that way I can prevent over fetching and larger network requests than I actually need. So we've got, now we've got our data coming in correctly. Let's go through and we're going to loop through the modules. So we'll do v 4 module in course dot modules, and then inside that we'll have another loop for the lessons.\u003C/p>\u003Cp>So we've got our second module. Let's style these a bit. So maybe we wrap this, do something like divide y and divide y, divide gray 300 maybe? Okay. So now we've got a divider between our different modules.\u003C/p>\u003Cp>And maybe we give each module some padding. That's probably a bit so we'll do p y 4. Let's style the module a bit And font, let's just give it a little fancier font. I think I've got this set up as maybe Poppins inside my specific account. Font display.\u003C/p>\u003Cp>Great. Div 4. Okay. We got the we don't really need the lesson content. And then let's style each one of the lessons.\u003C/p>\u003Cp>You know, we'll likely change these to a NUCs link and then the 2. Now we want to dive into each individual lesson. So our URL structure is going to be courses, we've got course, so we use the dollar sign, so so we get the template literals here. We've got course dot ID and then we've got lessons and then maybe lesson dot ID or it we could use slugs on the lessons as well if we wanted to. Let me fix this.\u003C/p>\u003Cp>We've got slugdot ID. Okay. Unterminated template literal. Okay. Alright.\u003C/p>\u003Cp>So now we've got a URL that should take us to this specific lesson. I'm gonna go back into our Directus instance, which keeps logging me out, probably a cookie or some type of setup issue on my end when I configured Docker, but we'll work around it for now. Let's go in and give a slug for the individual lessons as well. So, just to maintain parity. We'll go into the interface, we'll make slugify checked, and now we've got a slug for the lessons.\u003C/p>\u003Cp>Let's go into each one of those lessons and set a slug. Project templates, managing versions. Alright. Getting started. And custom operations.\u003C/p>\u003Cp>Okay. So now each one of those have a slug. We can change this from ID to slug. We'll still use that ID as the the key for looping over these. But now we're starting to get something that kind of looks like, some courses.\u003C/p>\u003Cp>You know, maybe we wrap these in a go back. Maybe we wrap these in a card component. And a lot of times as I go along, I would definitely be refactoring these as we went along into components that made sense. So we've got the padding, p y 4. Let's do some space between each card.\u003C/p>\u003Cp>So we've got a little bit of space between each card. That's great. There's our different modules. I don't know why the first module is showing, below the second module. That could be the way that we've got our sorting on the course.\u003C/p>\u003Cp>So if we go in, we don't have sorting enabled. So what I can do is go into our Courses section, our Courses Data Model, we'll go to the relationship field, or the relationship tab, and for our sort field we're just going to add that sort property, or that sort field on that. And that should take care of the sorting for us. So now we've got our first module, that's great. We've got our second module, starting to look like something.\u003C/p>\u003Cp>Let's just take one moment and make a, like a nice little header for this. We've got image course dot image, object cover, And we'll give this a little bit of padding as well. MT 8. Maybe we want to round the corners. Again, very crude but, definitely paints the picture.\u003C/p>\u003Cp>So we've got about 18 minutes left. Let's dive into the actual lessons, Right? So a pretty common set up when we go to the lessons, and right now we're getting a Page Error Not Found, is to have a list of the other lessons over here on the left hand side, and then on the right hand side we've got the actual course content. So let's figure out what that would actually look like inside here. So one of the nice things about Nuxt is like the nested routes.\u003C/p>\u003Cp>So we go to their documentation, we go to, it's like child routes. Child route keys, nested routes. So I can set up, like parent and child collections, or pages, within these nested routes so that, it's a nice way to do routing, where I can have a child page show up in the parent view. So let's go in and we've got our slug here. So this is our actual course.\u003C/p>\u003Cp>Within the course, maybe we want to show let's do a slug. I don't know if this will be it or not, slug dot view, So we'll create a new folder for lessons and then we're going to create a dynamic route for the lesson. So we'll call that lesson dot view v comp ts. We're going to use the route, so route equals use route. And here I'm just gonna log the params to make sure we're on the correct spot.\u003C/p>\u003Cp>Alright. So we can't find the slug dot view. That's great. Let's add a component for that. Courses, 100 apps, lessons, managing versions, slug.\u003C/p>\u003Cp>Let's just say this is the course parent. What does that get us? Alright. So we've got managing versions. We don't see the course parent part of it here.\u003C/p>\u003Cp>Okay. So let's call this lessons. And now all we should see is course parents. But if I go in, and I can't remember the exact syntax, so let's go back to nuxt.com. We'll do the child routes, nested routes.\u003C/p>\u003Cp>It is what? NUXT page. So we just throw this NUXT page in here And now we see this where we have Course Parent, and then we have our NUCs page which gets rendered inside that. So why are we doing it this way? Because we can actually fetch the lessons within this parent component and then, that will not re render, but then we can navigate within these individual lessons and render those over here on the right.\u003C/p>\u003Cp>So if we do something like this where, let's flesh this out, we've got a, let's do a div. Alright. We could do, like, an aside maybe. This is the list of lessons. Then we've got another div.\u003C/p>\u003Cp>We'll render that, and we'll just flex these. Flex. We'll add, let's just maybe give this a fixed width of like 56 or 64 wide. Alright. And just to illustrate this, we'll give it like a background, a bggray100.\u003C/p>\u003Cp>So we've got our list of lessons there. You Gonna add some padding for that, p 4. Then our next page, we probably got a new container. I need to actually look up order the the settings for that. Let's take a look at it.\u003C/p>\u003Cp>You container, constrain the width of your content. Max width 7 x l. Okay. Yeah. Works fine for me.\u003C/p>\u003Cp>Alright. And this could be probably height full. We want it to be the full height or height, screen. That'll get us. Cool.\u003C/p>\u003Cp>Alright. So now we can go in and render the list of lessons here. So we will grab our lessons. So let's do const lessons equals await. You got GitHub Copilot for the win.\u003C/p>\u003Cp>We're gonna read the items from the lessons. The filter is gonna be where the course dot slug equals route dot params.slug. So we're gonna pick up the we can pick up the course from the course slug. And let's actually see what we get here. I refresh the page.\u003C/p>\u003Cp>We're breaking some stuff. Course, slug, route. Oh, we actually have to grab the route, don't we? Forbidden. So I can't get this information.\u003C/p>\u003Cp>Wonder why that is. Let's take a look. We'll just erase that filter, see what data we're getting back, in this list of lessons. So I'm gonna quickly wrap that and, just output the lessons. Okay.\u003C/p>\u003Cp>So there's our individual lessons. Looks like those are actually coming through. Okay. But if we go to course dot slug is equal to, Let's just take a look at our network request. Alright?\u003C/p>\u003Cp>This is being fetched on the server side. You don't have a permission to access this, which makes me think something is wrong. Alright. So let's just look through this really quickly to see if we can find our module. Duh.\u003C/p>\u003Cp>That's because our modules are not, our lessons are not actually linked to our course, they are with our modules. So the the courses belong or the lessons belong to the individual modules. So, fun debugging issue here, but let's just go into our original index where we get the course, we get our lessons, and we can actually copy this code. And within the lesson route, her lessons where's our parent component? We could do this.\u003C/p>\u003Cp>Right. And here we could probably adjust it where the slug or the course, slug. So we get all the modules instead of the lessons, which is a little bit counterintuitive. But we still wanna display all those modules within that bar anyway. Alright.\u003C/p>\u003Cp>So we get our modules. We'll change this to modules. And we'll make sure that we grab the lessons as well. Let's just clean this up and refresh. Something is breaking.\u003C/p>\u003Cp>We don't get our modules. Route is not defined. Again, silly me, silly rabbit, you have to use the route. Alright. So now we can loop through these modules.\u003C/p>\u003Cp>And what does GitHub Copilot got for us? Alright. So we've got our different modules here, how to manage different versions, how operations can be completed. Okay, great. Nuxt link courses, route planarams.\u003C/p>\u003Cp>Slug. And now if I am navigating between these over here on the right you can see that this is actually changing. You can see that we're logging the slug for that specific lesson. So if we go into the lesson, we've got route params. Lesson.\u003C/p>\u003Cp>Now within this specific component we will call the lesson that we want. But what we'll do is make sure that we update this a little bit. And we're gonna use the lesson equals lessons if we just log lesson over here. Alright. Boom.\u003C/p>\u003Cp>We can see that. If I go back, I wanted to make sure this doesn't shrink. Select shrink. 0. Alright.\u003C/p>\u003Cp>And now on our individual lesson, let's actually set this up. So we got the lesson name. We got the lesson content. It's great. And then we will have a where it'll be an iframe?\u003C/p>\u003Cp>For the YouTube Embed. And I actually think do I have do I have Do I have a video string for this? I don't. Generate lesson and video. What is the YouTube embed?\u003C/p>\u003Cp>Alright. So we're getting the URL from YouTube. So if I log in to the lessons here, how do we transform this? Closing in on what? We're gonna cut this one down to the wire.\u003C/p>\u003Cp>Right? We've got 6 minutes and 55 seconds here. How do we actually render out this lesson content? We are going to do, let's rely on chat GPT or get up copilot. Right?\u003C/p>\u003Cp>Function to generate YouTube embed URL. Okay. So now we're gonna get youtubeurllesson.video. Let's see if this actually works. Cannot read properties of undefined split.\u003C/p>\u003Cp>So we'll do if, oh, I know what it is, we've used video URL instead of video. A lot of these issues can be avoided by using TypeScript, but lesson let's do a v f. Cannot read properties index of undefined. YouTube video ID. So we need to take a look at Directus.\u003C/p>\u003Cp>It's what's fun about doing these things live. You never know exactly what's gonna go wrong. And when you're under the gun, the pressure is mounting. Don't necessarily know. Can't even get the password right now.\u003C/p>\u003Cp>Hallelujah. There we go. Alright. So in this case, we want to, accounting for YouTube URL variations. Let's see what this thing comes up with.\u003C/p>\u003Cp>And you to that be Will this actually work? Hey. There we go. Alright. We probably want to do, like, an aspect ratio, aspect video.\u003C/p>\u003Cp>Okay. And so now we have our content. We could do a v if, so there is no lesson content here. And boom. So now if I click through these, we should be loading our different courses.\u003C/p>\u003Cp>So at this point we are at 3 minutes and 44 seconds. So we did not get as far along as I thought we would get on the front end of this. But, going in and extending this further, one of the things that I would do next is probably taking a look at building this out and adding enrollments to this. So we could set up a like authentication for this so that, let's just say on the access control side of it, instead of allowing all the courses to be seen we could certainly let people view all the courses, see the instructors. But as far as the actual lessons, maybe we didn't want to give them access to that until they were enrolled.\u003C/p>\u003Cp>But for anybody that was enrolled, they could see all of those courses. So now if I refresh the page, we're gonna see like an error that says, hey, this is forbidden because we cannot see this actual course. But if I were to go into my authentication page, let's just say auth/login and use my Directus URL or my Directus username and password. Go back a couple pages. Where are you?\u003C/p>\u003Cp>Courses. Alright. We go into that and we can actually see those. And that's because this user is that user is now authenticated as the administrator and they have full rights. But we could set up a public and then a user specific role where we've got courses, we've got instructors, you can see all of that, where we want to see only the, like, courses that that person has access to.\u003C/p>\u003Cp>So, let's just go with no access, like modules, that's fine. But as far as the lessons, you would have to set up like a a role here or, like a custom permission for this specific user. Taking that a step further, you know, we could get into like, making it work with Stripe, setting up some of the different, payment options. Like if we took a look at Udemy, there are, you know, trial options. You could pay individually for courses.\u003C/p>\u003Cp>You could set up subscriptions for this. But, clearly we we built the back end of our LMS, but, really a little disappointed on how far we made it through the front end, considering I've actually built several of these systems myself. But we've definitely learned a lot. So if we take a look at our list of functionality, how do we do? We got to view a list of courses, we got to the individual courses, we got the individual lessons, We did have the authentication and login, but as far as tracking progress, and enrolling for a course, I give myself, what, 4 out of 6 or 4 out of 7.\u003C/p>\u003Cp>Not a stellar effort here. So that is this lesson of 100 Apps, 100 Hours. I hope it's been a great example of how quickly you can build functionality like an LMS inside direct us and with front end tools. But there is a reason these other products exist like Podia, Kajabe, Udemy, Teachable, all of those. Rome wasn't built in 1 hour.\u003C/p>\u003Cp>So hope to catch you on the next episode. That's all I've got for you on this one.\u003C/p>","Welcome back to the next episode of 100 apps, 100 hours, where we try to recreate and build your favorite apps in 1 hour or less, or die trying. Hopefully not dying. I'm your host Brian Gillespie, developer advocate at Directus. Super nice to have you. Today we've got a app that is near and dear to my heart. I've created a lot of courses in the past, so we are going to be building a learning management system, an LMS. There's a few examples that I took a look at in the the research for this prior to. I've got my proper dad attire on and we are going to dive in and build some apps. So let's just add a little bit of context for you guys before we start the clock. If you take a look at sites like Kajabe or Podia, these are online learning management systems. Most of these are software as a service where you can sign up for, $159 a month, it looks like, or $199 a month and sell your courses. The interface for these, if we can maybe navigate to one of, Podia's websites here. We've got like a main navigation on the left. We've got, you know, the courses that show up on the right. If we take a look at Udemy, that's a kind of a different case where you're on their platform, you're not white labeling, but you can create your own courses and load those up and sell those to their specific audience. So I think we'll land somewhere in between all of these different tools today. Should be very fun, very interesting. So with that, let's, let's dive into this. I'm gonna start my little mouse pose thing so I can highlight things on screen as we go through and build. So, I want to kind of show you what I am working with just so there is no, you're aware, there's no cheating behind the scenes here. I do have a Directus instance already started. I haven't logged in yet, but that is a blank Instance ready to go. That's what we'll use for the back end of the app. We also have a Nuxt 3 starter application. I do have like a Directus SDK already preconfigured and a couple of utilities just to make building a little bit easier. But aside from that we can see this is just a blank slate. There's a login form, text component and an upload component, just to make this a little easier. A lot of this is boilerplate functionality, I don't want to bore you guys with that. So that's the setup. Alright. Let's dive into building the app and we will start the clock on this. So we now have 60 minutes to develop this out, But before we actually do any code, I want to like sketch this out. And I I love using Figma for this. So the first thing we want to flesh out is our data model. What's our schema? We're going to have courses. So within each course, we have some lessons. We probably have some modules. Alright. Maybe these are not huge per se. We'll just make them smaller. What else are we gonna have? We're gonna have users. We'll probably have some instructors. Right? We want to be able to track the instructors on the course. This is probably a a pretty good starting point. Now as far as the functionality that we need, so let's just call this our data model. Cool. We'll go over here. Inspect this out. Functionality. Alright. Let's go through and flesh this out a little bit. We want to view a list of courses, view an individual course, and this is gonna be on the front end. View individual lessons, we probably need to sign up for our course. We just walk it through the entire process. View individual lessons. We want some authentication, so we wanna be able to log in. And, what else? We wanna be able to track progress. Right? And, to that end we probably need like a collection to track enrollments. So who are our users, what courses are they enrolled in, what is the completion percentage for those. Alright. So this seems like a pretty good start. We'll continually come back and refer to this and refine it as we go through. But the first thing that I wanna do is start into our back end. So I'm gonna pull up Directus in this case, and let's see if we can zoom in just a little bit to make it easier for everybody. Now I've got this running on Docker locally, but the easiest way to run Directus is using Directus Cloud. And I need to make sure I've got the correct password here. Alright. So we've got a blank Directus instance. We're going to flesh out this data model in no time at all. So let's create our 1st collection, we'll call it Courses, and we will use the generated UUID as the primary key field. We'll just go in and for now we'll just select all of these defaults for tracking date created, user created, etcetera, handy to have. And for our courses we probably want a name for the course, that's great. We want to have a description for the course, so that will be a WYSIWYG editor. That way we could support HTML and rich text formatting. What else do we need as as far as our course data? We've probably got an image for the course as well. So we could call this image. We could have multiple images, but let's just stick with a single featured image, and we'll use that. Great. So now if we back up, we've got the start to our courses data model. Let's go in and flesh out the other parts of our course. So each course will be made up of several different modules. So the modules, again, we can add all these fields. We may not need those. I typically just add these because it has some preset functionality. Obviously the user created it will store the UUID for that particular user, updated date and times, those are all handy to have. So the modules, we will have a name for the module, and, you know, we could have like a short description for the module. Let's just stick with name, we'll keep it really simple. The other thing that we may want to have on the courses, this just reminds me, is maybe something like a slug. So just a pretty URL for that specific course. So we'll go in and set that up. One of the other things I could do really quickly in Directus is make sure this is URL safe. So there's just a, a field to set that up for us to automatically format that. Next, let's go in and add our lessons table, or lessons collection. We'll do the same, so what's the status, is this published or is it draft? And now in the lessons we'll have a name for the lesson. A lot of names here. We'll have the, let's call it Lesson Content, and that could be rich text. And because these are primarily videos, let's do a video URL. We can host those on Vimeo, YouTube, Bloom, however you wanted to do that. You could also just actually upload these into Directus as well. Alright, so now we've got kind of this side of the equation figured out. Directus already gives us users out of the box, which is great. We can go in and add these other collections. We'll have an instructor's collection. Instructor's, I hope that's how you spell it. So our course instructors, that will be we'll have a name for them. Let's have a bio, so that'll be a WYSIWYG field. And one of the things that I love most about Directus is just how easy it is to map out this data model and get a complete back end with an AI or API ready to query for my front end. So instead of working with dummy data, I can actually prototype live with real data. So we got our name, we got our bio, let's do an image, or we could call it avatar. Great. Cool. And last but not least, we've got enrollments. So we'll just, create all these separate tables, all these different collections within Directus. And now we can go back and flesh out the relationships between these, because that's where the tricky part comes in. Or not necessarily tricky with Directus, but, we wanna make sure that data model works correctly on the front end. So the when we start mapping these out we have our courses. Each course could have multiple modules, and each module could have multiple lessons. So those are one too many relationships inside Directus. So if we start fleshing this out, we'll go into the Courses collection and we'll use the relational fields inside Directus. So behind the scenes Directus will set up these relationships inside your SQL database and make sure everything plays nice with each other. So we're going to call this modules. So that's going to be our key. The related collection here is going to be modules. And then the foreign key, so our key inside the modules collection is going to be Course. We could choose whether we want a list or table layout, that's fine, and we'll just use the name of the module and we'll show a link to that module. So now our modules are set up and if I were to go to the actual module section we can see we've got the reverse relationship back to the course. And maybe we drag and drop this, give these some icons to make them look nice. So we've got a Course, maybe that is a folder looking object. It's great. Let's use black as the color. So we'll customize this a bit and make it look nice. The module, you know, let's look for a list, maybe. Looks great. And now let's add a lesson, we'll just use like a video icon for that. Perfect. Okay. So now if we go back to our modules, again we're going to create a one to many relationship, but this time we're going to do that on our lessons. So the key here will be called lessons and the related collection will be lessons. Our foreign key will be module. So just the singular because each lesson can only be in a single module. And we'll use the name of that lesson. Great. We'll show a link to the item. And now we've got that structure set up. Great. How do the other items relate? Right? We've got instructors. Instructors could have many different courses inside our platform, and a course could have multiple instructors. So that's a good use case for the many to many relationships inside Directus. And what we'll do is go to either courses or instructors, doesn't really matter which one. And instead of a one to many or a many to 1, we're going to use the many to many relationship. So here we'll call this instructors, that's gonna be our key. For our related collection, we're gonna use instructors. And we don't want to allow duplicates, we want to show a link to the item. If I wanted more control over what Directus creates, or uses for the name of the junction tables, I can go into the advanced field mode. So we can see in our junction collection Directus is going to create this inside our SQL database. Courses underscore instructors, and there's gonna be a Courses ID and an Instructor's ID. I can also add this relationship to the instructors field and we'll give this a sort just in case we want to have a head instructor and, you know, prioritize who is the primary instructor on the course. So we'll hit save, Directus will do some magic behind the scenes, and if I go in now I will see a hidden collection in our data model for courses underscore instructors. Savvy? Everything's looking good so far? Great. Alright, now we also want to flesh out our enrollments. So if we think of enrollments, enrollments are going to be the users that are enrolled in the individual courses and this setup will be a little bit different in that we go to Enrollments, there's gonna be a many to one relationship. So each enrollment can only have one course. So what are the course that this person enrolled in? Great. And then we're also going to have a many to one relationship with the Directus user. In this case we could just call it user as well. But the related collection here is going to be the Directus underscore users. That's our system collection for users within directus that, gives us all the authentication and user access and permissions. We could also keep track of progress or lessons completed, but for now let's just roll with this. Okay, a little OCD over things like icons and colors, so I will add some icons and colors here. We've got instructors. These will be people. Let's see what we've got. Nice friendly guy waving, that's perfect. Let's make that purple as well. Alright, so now if we look at our data model, this is looking pretty good. We will go into the front end and we can see that we've got courses, we've got modules, we've got lessons. Let's just create a course. Let's do published. And we'll call this 100 Apps in 100 hours, great. Watch to follow along as we build an LMS. While building an LMS? I don't know. It's kinda meta. No worries. Alright so we can, let's go to Unsplash and find a nice looking image. One of the nice things about Directus, and you'll hear me say that multiple times throughout this series, is the ability to import from a URL. So I can just copy that URL there, import that image, it will store it in my file library for me. And we'll give this a slug. So you'll see if I press space bar it will automatically format this for me so that it becomes URL safe. And let's go in and create a module. So we'll call this 1st module. We'll create a second module. Great. And we'll add an instructor. The dude building stuff and videos. Cool. I've got let's upload a photo. Great. Good. Solid. Okay. So now we've got our first course. Since we've set up these collections I could start querying this on the front end using the Directus APIs. So So if I just open this up in a new tab, I'm going to change the URL a little bit. We'll go to items/courses and boom, we get this permission access error. So I haven't enabled permissions. We've created this actual, collection. We've created several of these collections, but for the general public all these collections are access is forbidden by default. So I could open this up and you know, just allow public access for now to all of these. Once we went to production, obviously we want to restrict this per user. But if I just refresh you can see over here on the left, excuse me, we've got our data coming from the LMS, or the back end of our LMS, our direct us instance. Great. So we are about 15 minutes in. We've got our first bit of data modeling done. Let's dive into kind of the front end and start building, right? We will, I'm just gonna move this out of the way for now, and we'll get to a blank slate inside our code editor. We've got our direct assistance up here, but let's pull this up. Now within the pages directory of my Nuxt application, maybe we create a new folder for courses. This will give us a route slash courses on the front end. And we'll have an index route. That's great. We'll set up a default view component here and now we can start querying that data from Directus. One of the things that I do have in my little Directus module is a composable to actually query the front end of or query the Directus back end. So we can do something like this where we say const courses equals useDirectus. And this is using the Directus SDK on the the back end. We're gonna read items, courses, And I could go in and add some options here as well. You know, Copilot is showing me like a filter for published. But for now, because we haven't published anything, let's just take a look at this. We'll, log this out to the front end. And if we navigate to /courses, it looks like we have an object promise, so we wanna do await use directus, and boom, now we've got our data on the front end. Cool. So now we probably want to do a little bit of formatting, you know, add this, add maybe a card for this. One of the things that I am using or I've got set up in this Nuxt instance or this Nuxt template app is the Nuxt UI library, which is just a UI library put out by the Nuxt team. It helps you build apps that look great faster. It works perfectly for stuff like this. On a lot of other projects, I may use, you know, my own custom coded components. But for now let's do a card. We'll do v4 courses and courses because that is an array, course ID. Let's actually just do something like this where we've got v text. I do also have a text component, we'll call it course. Name. And we can use the Nuxt image component, course. Image. See how far that gets us. Okay. So we've got a card. We probably want to give a little bit of styling to the container. What is the I guess the muxu container It constrains the width. Okay. And then maybe we've got let's just go back. We'll do the keep this really simple. I also, use tailwind a lot. We'll do like a max width of 4 XL for now. Make this in x auto. And maybe we set these up in a grid with 3 columns on, like a large size. Okay. So we've got this actual course card. We're not rendering the actual image, which, is concerning me. You don't have permission to access this. So we've hit our first kind of snag here. Somehow I've managed to log myself out of Directus as well. So let's log back in. Directus. Alright. And now we will go in and we forgot to set permissions for our system collection. So all the Directus files, we're just going to make those public for now so we can take a look at that file. I could now see that and it should show up within our card as well. Great. You know, maybe we make this font bold, 2 x l. Make it a bit bigger. Let's make it giant, right, 4 x l. I need to take a look at my Vtex component actually. I think I've already got sizes on here, so we could just use the size prop instead. 2XL. Great. Okay. Alright. So now we've got kind of an index page. This is a course listing. Let's wrap this with, another div. I'll go in and paste that. So now we've got a course listing, we've got a course card, How do we get to this actual course detail? So I will go in and, now we'll go into our courses folder within the pages directory And let's create actually let's create a new folder. And we'll use the brackets for this. This will give us a dynamic route. And we will use like slug or I could call this course or ID. We're using slug as the property for the course on the directive side, if I remember correctly. So we've got a slug for each of the courses. That's what we'll query by. Sounds great. And we will go in and now we will add like an index page for this course. So again, we'll build a detailed page here, just a simple index page, and let's do course, wait, use direct us, read item. We wanna read a single item. This is using our SDK context. And we'll use route params slug. That's not gonna cut it. Actually, we want to read multiple items. And the reason why is because we're giving each course an ID, a a UUID. And the Directus SDK, when you read a single item, you can look up by that primary key, in this case the ID field. But where we want to look up by the slug, we're going to use read items and then we're going to construct a filter. So it'll be like this where slug is equal to route dot params.slug. We're gonna use route. So we'll pick up the route from view. We use route dot params.slug. And again, this is why I love building this way with the back end first because I can use actual data to flesh this out. So now on our course listing page we want to add a link to that. So if we go back to the index page, we can wrap, both of these in a Nuxt link component. And for the to prop, let's just add, courses slash course ID or no, course dot slug. Alright, so now we should get a clickable link. We can see at the bottom of the screen it says 100 Apps, 100 Hours. And when I click on it it will give me, an array of the data in this case. But I would rather have just a, I'd rather have the actual image. So let's do something like this where constant course equals courses dot I or core the first item of the array. Maybe this could be a computer prop as well just in case that changes. Courses dot value cannot read properties of undefined. What is going on here? Let's back up and let's just keep it simple. Right. Course equals courses. Okay, there we go. So we've got our course, and now we can start fleshing this out. So if I go in and I look at the Nuxt UI Library, we've got some components here, we've got like a skeleton. We've got a container. We've got a card. What do some of these other apps look like? How do we actually get to their courses? Not a lot of great examples on the website. If we look at Udemy, we could see we've got like a course name, we've got a preview of the course, we've got, some of the lessons here. And then when you get into the actual, enrollment of that specific course, they've got like a kind of a sidebar layout. So let's create a new section. We'll do the course title, course description, follow along as we build. Okay. 100 apps, 100 hours. So the course description, we're gonna use v html for that. Course dot description. Okay. And now we've got that. We could apply some nice styling for this. Let's do the font. Black text 4 x l. We'll do a, let's try that U container again. And this is coming from the Nuxt UI library. Alright. So we got that. We've got, the description. Maybe we wanna add like a small bit of text to label that. This description class text extra small, text gray 500, something like that. And we can space these out a bit. So again, this is a bit crude. We're going for speed over beauty in this case. But we want to flesh out this functionality. Where did you go? Okay, so we got our course name, we got a description. Let's show our instructors. Alright. So this is gonna be our pclasstext. Let's do instructors. Okay. So we've got that. We'll add a bit of margin to it. Space it apart. We'll pull up the instructors. How are we doing on time? Quick time check. Got 30 minutes remaining to flesh this out. Gonna be a definite challenge here. So we've got the instructors. We'll do, should be course dot instructors, right? Let's just log that out and see what we get. Course dot instructors. So you can see Directus is giving me back an array of the different ID's for those instructors. I've said this multiple times, but this is one of the reasons why I really love Directus. Because I can go in and for, this is using the REST API. I can actually go in and do something like this where I say, hey, I want all the root level fields. And then for instructors, I want, I want a list of all the root level fields for the instructor. So I could get all the related fields in a single API call. Now where this is a many to many relationship, you can see I need to drill in one more level. So I have to go in and do one more level where I have instructors underscore ID and grab all those fields. So now I can see the instructor and we can set up like a, just a v four loop here. V four instructor in course dot instructors. And it looks like Copilot has added a lot of code that I did not mean to add here. So let's just pull that back. Be careful how many times you tab, right? So now let's take a look at instructor within that loop and see what we have. Are we even getting anything? Alright. We've got instructors. We've got the instructor ID. K. So we could do something like this. We could actually destructure that. Or can we? Instructor instructor's ID. And let's see what we get back with that. We're breaking stuff. Okay. Yeah. So we'll just run with this for now. Alright. So we've got our instructor, we've got an instructor ID. So we'll have instructor dot instructorsid.name. Let's wrap that P TECH. We'll do, Nuxt Image. And let's do, yeah. Maybe width 24 sounds good. 24 height, rounded full to make this a circle. And then the source is gonna be instructor dot instructorsid.avatar. And then we can do a divvhtml for the bio equals instructor dot instructorsid.bio. Okay. Cool. So now we've got a few things going on here. Let's actually look at the lessons. Right? We'll go into our Directus instance. If we go back to our specific course here, we didn't add any lessons to this specific module. So I'm just gonna pull up the Directus YouTube account. Our YouTube studio here will get to the Directus channel and take a look at our content. So I'm just gonna copy a few of these down. We're gonna add a couple of lessons within each one of these. Just gonna actually take these wholesale from the YouTube channel. Here's some content. Great. Alright. How do we do, we'll copy some of this. How to manage different versions. Blah blah blah, copy that video URL. Oh, that's actually a short. Not sure what that'll do. Let's, let's take an actual video. Great. So we've got our first module. Let's go into the second module and we will add one of my videos. Getting started with Agency OS. My wife tends to make fun of, my sausage fingers all the time. So lots of typos here. So we'll go in. Let's just copy one more of these so we've got something to look at on the front end. Project templates. Great. Okay. So So save this. Now we've got our course, we go back to our course detail page in this instance and, you know, let's add a new section over here where we've got our section. Looks like it get hub copilot for the win. We've got our lessons. Give that some margin. Give some space. And, so if we log each lesson, let's just take a look, we'll probably get the same, let's just close this out real quick. We'll do course dot lessons just to see what we're getting back from the API, which it doesn't look like anything, right? So if I open up the dev tools, we take a look at the network request, we dive into the data, the lessons are actually going to come through the different modules. Alright, so again, we can go back up to our fields section and we could do something like this where we go to modules, then we, drill down again into, our lessons. And we want to grab the module name. And within our lessons, again, we could just grab all the root level fields within that. And now we can see that second API call that was made. We've got our modules. We've got our lessons within that. And again, I only have to make a single API call. You know, we can certainly cache this if this data doesn't change, but it keeps the entire app very snappy and prevents me from having to make 35 different calls to get all the related data. Also kind of it follows a very GraphQL like structure as well where I can request just the data that I need. I don't necessarily have to go in and use these wildcards to grab those root level fields. I could go in and say something like name, description, that way I can prevent over fetching and larger network requests than I actually need. So we've got, now we've got our data coming in correctly. Let's go through and we're going to loop through the modules. So we'll do v 4 module in course dot modules, and then inside that we'll have another loop for the lessons. So we've got our second module. Let's style these a bit. So maybe we wrap this, do something like divide y and divide y, divide gray 300 maybe? Okay. So now we've got a divider between our different modules. And maybe we give each module some padding. That's probably a bit so we'll do p y 4. Let's style the module a bit And font, let's just give it a little fancier font. I think I've got this set up as maybe Poppins inside my specific account. Font display. Great. Div 4. Okay. We got the we don't really need the lesson content. And then let's style each one of the lessons. You know, we'll likely change these to a NUCs link and then the 2. Now we want to dive into each individual lesson. So our URL structure is going to be courses, we've got course, so we use the dollar sign, so so we get the template literals here. We've got course dot ID and then we've got lessons and then maybe lesson dot ID or it we could use slugs on the lessons as well if we wanted to. Let me fix this. We've got slugdot ID. Okay. Unterminated template literal. Okay. Alright. So now we've got a URL that should take us to this specific lesson. I'm gonna go back into our Directus instance, which keeps logging me out, probably a cookie or some type of setup issue on my end when I configured Docker, but we'll work around it for now. Let's go in and give a slug for the individual lessons as well. So, just to maintain parity. We'll go into the interface, we'll make slugify checked, and now we've got a slug for the lessons. Let's go into each one of those lessons and set a slug. Project templates, managing versions. Alright. Getting started. And custom operations. Okay. So now each one of those have a slug. We can change this from ID to slug. We'll still use that ID as the the key for looping over these. But now we're starting to get something that kind of looks like, some courses. You know, maybe we wrap these in a go back. Maybe we wrap these in a card component. And a lot of times as I go along, I would definitely be refactoring these as we went along into components that made sense. So we've got the padding, p y 4. Let's do some space between each card. So we've got a little bit of space between each card. That's great. There's our different modules. I don't know why the first module is showing, below the second module. That could be the way that we've got our sorting on the course. So if we go in, we don't have sorting enabled. So what I can do is go into our Courses section, our Courses Data Model, we'll go to the relationship field, or the relationship tab, and for our sort field we're just going to add that sort property, or that sort field on that. And that should take care of the sorting for us. So now we've got our first module, that's great. We've got our second module, starting to look like something. Let's just take one moment and make a, like a nice little header for this. We've got image course dot image, object cover, And we'll give this a little bit of padding as well. MT 8. Maybe we want to round the corners. Again, very crude but, definitely paints the picture. So we've got about 18 minutes left. Let's dive into the actual lessons, Right? So a pretty common set up when we go to the lessons, and right now we're getting a Page Error Not Found, is to have a list of the other lessons over here on the left hand side, and then on the right hand side we've got the actual course content. So let's figure out what that would actually look like inside here. So one of the nice things about Nuxt is like the nested routes. So we go to their documentation, we go to, it's like child routes. Child route keys, nested routes. So I can set up, like parent and child collections, or pages, within these nested routes so that, it's a nice way to do routing, where I can have a child page show up in the parent view. So let's go in and we've got our slug here. So this is our actual course. Within the course, maybe we want to show let's do a slug. I don't know if this will be it or not, slug dot view, So we'll create a new folder for lessons and then we're going to create a dynamic route for the lesson. So we'll call that lesson dot view v comp ts. We're going to use the route, so route equals use route. And here I'm just gonna log the params to make sure we're on the correct spot. Alright. So we can't find the slug dot view. That's great. Let's add a component for that. Courses, 100 apps, lessons, managing versions, slug. Let's just say this is the course parent. What does that get us? Alright. So we've got managing versions. We don't see the course parent part of it here. Okay. So let's call this lessons. And now all we should see is course parents. But if I go in, and I can't remember the exact syntax, so let's go back to nuxt.com. We'll do the child routes, nested routes. It is what? NUXT page. So we just throw this NUXT page in here And now we see this where we have Course Parent, and then we have our NUCs page which gets rendered inside that. So why are we doing it this way? Because we can actually fetch the lessons within this parent component and then, that will not re render, but then we can navigate within these individual lessons and render those over here on the right. So if we do something like this where, let's flesh this out, we've got a, let's do a div. Alright. We could do, like, an aside maybe. This is the list of lessons. Then we've got another div. We'll render that, and we'll just flex these. Flex. We'll add, let's just maybe give this a fixed width of like 56 or 64 wide. Alright. And just to illustrate this, we'll give it like a background, a bggray100. So we've got our list of lessons there. You Gonna add some padding for that, p 4. Then our next page, we probably got a new container. I need to actually look up order the the settings for that. Let's take a look at it. You container, constrain the width of your content. Max width 7 x l. Okay. Yeah. Works fine for me. Alright. And this could be probably height full. We want it to be the full height or height, screen. That'll get us. Cool. Alright. So now we can go in and render the list of lessons here. So we will grab our lessons. So let's do const lessons equals await. You got GitHub Copilot for the win. We're gonna read the items from the lessons. The filter is gonna be where the course dot slug equals route dot params.slug. So we're gonna pick up the we can pick up the course from the course slug. And let's actually see what we get here. I refresh the page. We're breaking some stuff. Course, slug, route. Oh, we actually have to grab the route, don't we? Forbidden. So I can't get this information. Wonder why that is. Let's take a look. We'll just erase that filter, see what data we're getting back, in this list of lessons. So I'm gonna quickly wrap that and, just output the lessons. Okay. So there's our individual lessons. Looks like those are actually coming through. Okay. But if we go to course dot slug is equal to, Let's just take a look at our network request. Alright? This is being fetched on the server side. You don't have a permission to access this, which makes me think something is wrong. Alright. So let's just look through this really quickly to see if we can find our module. Duh. That's because our modules are not, our lessons are not actually linked to our course, they are with our modules. So the the courses belong or the lessons belong to the individual modules. So, fun debugging issue here, but let's just go into our original index where we get the course, we get our lessons, and we can actually copy this code. And within the lesson route, her lessons where's our parent component? We could do this. Right. And here we could probably adjust it where the slug or the course, slug. So we get all the modules instead of the lessons, which is a little bit counterintuitive. But we still wanna display all those modules within that bar anyway. Alright. So we get our modules. We'll change this to modules. And we'll make sure that we grab the lessons as well. Let's just clean this up and refresh. Something is breaking. We don't get our modules. Route is not defined. Again, silly me, silly rabbit, you have to use the route. Alright. So now we can loop through these modules. And what does GitHub Copilot got for us? Alright. So we've got our different modules here, how to manage different versions, how operations can be completed. Okay, great. Nuxt link courses, route planarams. Slug. And now if I am navigating between these over here on the right you can see that this is actually changing. You can see that we're logging the slug for that specific lesson. So if we go into the lesson, we've got route params. Lesson. Now within this specific component we will call the lesson that we want. But what we'll do is make sure that we update this a little bit. And we're gonna use the lesson equals lessons if we just log lesson over here. Alright. Boom. We can see that. If I go back, I wanted to make sure this doesn't shrink. Select shrink. 0. Alright. And now on our individual lesson, let's actually set this up. So we got the lesson name. We got the lesson content. It's great. And then we will have a where it'll be an iframe? For the YouTube Embed. And I actually think do I have do I have Do I have a video string for this? I don't. Generate lesson and video. What is the YouTube embed? Alright. So we're getting the URL from YouTube. So if I log in to the lessons here, how do we transform this? Closing in on what? We're gonna cut this one down to the wire. Right? We've got 6 minutes and 55 seconds here. How do we actually render out this lesson content? We are going to do, let's rely on chat GPT or get up copilot. Right? Function to generate YouTube embed URL. Okay. So now we're gonna get youtubeurllesson.video. Let's see if this actually works. Cannot read properties of undefined split. So we'll do if, oh, I know what it is, we've used video URL instead of video. A lot of these issues can be avoided by using TypeScript, but lesson let's do a v f. Cannot read properties index of undefined. YouTube video ID. So we need to take a look at Directus. It's what's fun about doing these things live. You never know exactly what's gonna go wrong. And when you're under the gun, the pressure is mounting. Don't necessarily know. Can't even get the password right now. Hallelujah. There we go. Alright. So in this case, we want to, accounting for YouTube URL variations. Let's see what this thing comes up with. And you to that be Will this actually work? Hey. There we go. Alright. We probably want to do, like, an aspect ratio, aspect video. Okay. And so now we have our content. We could do a v if, so there is no lesson content here. And boom. So now if I click through these, we should be loading our different courses. So at this point we are at 3 minutes and 44 seconds. So we did not get as far along as I thought we would get on the front end of this. But, going in and extending this further, one of the things that I would do next is probably taking a look at building this out and adding enrollments to this. So we could set up a like authentication for this so that, let's just say on the access control side of it, instead of allowing all the courses to be seen we could certainly let people view all the courses, see the instructors. But as far as the actual lessons, maybe we didn't want to give them access to that until they were enrolled. But for anybody that was enrolled, they could see all of those courses. So now if I refresh the page, we're gonna see like an error that says, hey, this is forbidden because we cannot see this actual course. But if I were to go into my authentication page, let's just say auth/login and use my Directus URL or my Directus username and password. Go back a couple pages. Where are you? Courses. Alright. We go into that and we can actually see those. And that's because this user is that user is now authenticated as the administrator and they have full rights. But we could set up a public and then a user specific role where we've got courses, we've got instructors, you can see all of that, where we want to see only the, like, courses that that person has access to. So, let's just go with no access, like modules, that's fine. But as far as the lessons, you would have to set up like a a role here or, like a custom permission for this specific user. Taking that a step further, you know, we could get into like, making it work with Stripe, setting up some of the different, payment options. Like if we took a look at Udemy, there are, you know, trial options. You could pay individually for courses. You could set up subscriptions for this. But, clearly we we built the back end of our LMS, but, really a little disappointed on how far we made it through the front end, considering I've actually built several of these systems myself. But we've definitely learned a lot. So if we take a look at our list of functionality, how do we do? We got to view a list of courses, we got to the individual courses, we got the individual lessons, We did have the authentication and login, but as far as tracking progress, and enrolling for a course, I give myself, what, 4 out of 6 or 4 out of 7. Not a stellar effort here. So that is this lesson of 100 Apps, 100 Hours. I hope it's been a great example of how quickly you can build functionality like an LMS inside direct us and with front end tools. But there is a reason these other products exist like Podia, Kajabe, Udemy, Teachable, all of those. Rome wasn't built in 1 hour. So hope to catch you on the next episode. That's all I've got for you on this one.","published",[139],{"people_id":140},{"id":141,"first_name":142,"last_name":143,"avatar":144,"bio":145,"links":146},"791e1503-1d88-463d-9347-0b9192933576","Bryant","Gillespie","9013afc8-e8d7-4182-9b18-44db08117bb9","Developer Advocate at Directus",[147,149],{"url":131,"service":148},"website",{"service":150,"url":151},"github","https://github.com/bryantgillespie",[],{"id":154,"number":155,"year":156,"episodes":157,"show":168},"56dda5ff-2c3a-41ce-ae3a-580d6101026b",1,"2023",[158,122,159,160,161,162,163,164,165,166,167],"cb4e067f-9507-4e18-ab9a-435565f9e653","c997b25e-400c-4350-bba4-f63853d844f7","1109be0d-8ab5-479b-a052-8ad30d9ffb1c","dcb952e0-7d13-43ea-9b1c-f2ca63efd07d","0aae287d-3916-4d91-9310-25828998e562","8fed0eee-43f6-4767-8b77-79da7a059821","a311b57d-34e7-4073-9cf3-6a8c6c0f8b85","9271a4fc-addf-4400-9591-f2f2ec07bd79","30c48566-52cd-4728-a633-ca9675acc959","c067c012-5fe1-4d70-8946-8bfc8e1b22c0",{"title":169,"tile":170},"100 Apps In 100 Hours","fb0f9d45-be21-4634-94d4-2ef1cc5146f2",{"id":172,"slug":173,"season":174,"vimeo_id":175,"description":176,"tile":177,"length":178,"resources":8,"people":8,"episode_number":155,"published":179,"title":180,"video_transcript_html":181,"video_transcript_text":182,"content":8,"seo":183,"status":137,"episode_people":184,"recommendations":186},"9a3a8ffa-a27b-421c-93cf-3da2dcb726e9","crm","14fda5f2-95de-4dbe-a4e2-3fd956c21c19","936325383","In this intense one-hour challenge, watch as Bryant incredibly builds a full-featured custom CRM from the ground up using Directus. He builds contacts, organizations, deal pipelines, activities, and more.","f6b880a0-5cd2-45ad-beff-3117f0a78581",56,"2024-04-19","Mission: Customer Relationship Manager","\u003Cp>Speaker 0: Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season.\u003C/p>\u003Cp>If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality.\u003C/p>\u003Cp>That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM.\u003C/p>\u003Cp>A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go.\u003C/p>\u003Cp>So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to.\u003C/p>\u003Cp>What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up.\u003C/p>\u003Cp>So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people.\u003C/p>\u003Cp>I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities.\u003C/p>\u003Cp>Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality.\u003C/p>\u003Cp>Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection.\u003C/p>\u003Cp>Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right?\u003C/p>\u003Cp>When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by.\u003C/p>\u003Cp>That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright.\u003C/p>\u003Cp>So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time.\u003C/p>\u003Cp>And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with.\u003C/p>\u003Cp>Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation.\u003C/p>\u003Cp>So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields.\u003C/p>\u003Cp>Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title.\u003C/p>\u003Cp>And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string.\u003C/p>\u003Cp>Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great.\u003C/p>\u003Cp>K. Phone, email. We'll put email above phone. Cool. Looks nice.\u003C/p>\u003Cp>Alright. Let's just take a look. Right? We've got first name. Go ahead.\u003C/p>\u003Cp>Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact.\u003C/p>\u003Cp>These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations.\u003C/p>\u003Cp>So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great.\u003C/p>\u003Cp>Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right?\u003C/p>\u003Cp>Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization?\u003C/p>\u003Cp>You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company.\u003C/p>\u003Cp>So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save.\u003C/p>\u003Cp>So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that?\u003C/p>\u003Cp>Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop.\u003C/p>\u003Cp>So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship.\u003C/p>\u003Cp>And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that.\u003C/p>\u003Cp>So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations.\u003C/p>\u003Cp>Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right?\u003C/p>\u003Cp>On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great.\u003C/p>\u003Cp>So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here.\u003C/p>\u003Cp>Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes.\u003C/p>\u003Cp>I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right?\u003C/p>\u003Cp>So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection.\u003C/p>\u003Cp>Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright.\u003C/p>\u003Cp>So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay.\u003C/p>\u003Cp>So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league.\u003C/p>\u003Cp>We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay.\u003C/p>\u003Cp>So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great.\u003C/p>\u003Cp>Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this.\u003C/p>\u003Cp>So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at.\u003C/p>\u003Cp>Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great.\u003C/p>\u003Cp>Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data.\u003C/p>\u003Cp>So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right?\u003C/p>\u003Cp>We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields.\u003C/p>\u003Cp>But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal.\u003C/p>\u003Cp>So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here.\u003C/p>\u003Cp>We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact.\u003C/p>\u003Cp>The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes.\u003C/p>\u003Cp>We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially.\u003C/p>\u003Cp>Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right?\u003C/p>\u003Cp>Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection.\u003C/p>\u003Cp>Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it.\u003C/p>\u003Cp>And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages.\u003C/p>\u003Cp>Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application.\u003C/p>\u003Cp>So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right?\u003C/p>\u003Cp>Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red.\u003C/p>\u003Cp>This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted?\u003C/p>\u003Cp>Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that.\u003C/p>\u003Cp>Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo.\u003C/p>\u003Cp>Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got?\u003C/p>\u003Cp>Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray.\u003C/p>\u003Cp>Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here.\u003C/p>\u003Cp>So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great.\u003C/p>\u003Cp>If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value.\u003C/p>\u003Cp>So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like.\u003C/p>\u003Cp>Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right?\u003C/p>\u003Cp>So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on.\u003C/p>\u003Cp>I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright.\u003C/p>\u003Cp>Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need.\u003C/p>\u003Cp>Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage.\u003C/p>\u003Cp>That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there?\u003C/p>\u003Cp>1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact.\u003C/p>\u003Cp>Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh.\u003C/p>\u003Cp>Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale.\u003C/p>\u003Cp>Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on.\u003C/p>\u003Cp>Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay.\u003C/p>\u003Cp>Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display.\u003C/p>\u003Cp>So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back.\u003C/p>\u003Cp>We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail.\u003C/p>\u003Cp>That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL.\u003C/p>\u003Cp>Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool.\u003C/p>\u003Cp>And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout.\u003C/p>\u003Cp>Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created.\u003C/p>\u003Cp>I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here?\u003C/p>\u003Cp>Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right?\u003C/p>\u003Cp>So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great.\u003C/p>\u003Cp>And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense.\u003C/p>\u003Cp>We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone.\u003C/p>\u003Cp>One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call.\u003C/p>\u003Cp>The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way.\u003C/p>\u003Cp>Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting.\u003C/p>\u003Cp>And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright.\u003C/p>\u003Cp>So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email?\u003C/p>\u003Cp>Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great.\u003C/p>\u003Cp>Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good.\u003C/p>\u003Cp>Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived.\u003C/p>\u003Cp>What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right?\u003C/p>\u003Cp>Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed.\u003C/p>\u003Cp>Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date.\u003C/p>\u003Cp>Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right?\u003C/p>\u003Cp>So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal.\u003C/p>\u003Cp>Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright.\u003C/p>\u003Cp>So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities.\u003C/p>\u003Cp>We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type.\u003C/p>\u003Cp>Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment.\u003C/p>\u003Cp>But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to.\u003C/p>\u003Cp>Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner.\u003C/p>\u003Cp>Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well.\u003C/p>\u003Cp>Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay.\u003C/p>\u003Cp>Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright.\u003C/p>\u003Cp>Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now.\u003C/p>\u003Cp>I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar.\u003C/p>\u003Cp>Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah.\u003C/p>\u003Cp>This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that.\u003C/p>\u003Cp>And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good.\u003C/p>\u003Cp>Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that.\u003C/p>\u003Cp>And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright.\u003C/p>\u003Cp>Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr.\u003C/p>\u003Cp>Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that.\u003C/p>\u003Cp>And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool.\u003C/p>\u003Cp>Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right?\u003C/p>\u003Cp>If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see.\u003C/p>\u003Cp>Business. Is there a business? There you go. That looks somewhat like a business. We've got activities.\u003C/p>\u003Cp>Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great.\u003C/p>\u003Cp>And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here.\u003C/p>\u003Cp>That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings.\u003C/p>\u003Cp>So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome.\u003C/p>\u003Cp>Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks.\u003C/p>\u003Cp>We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man.\u003C/p>\u003Cp>And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is.\u003C/p>\u003Cp>It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed.\u003C/p>\u003Cp>That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win.\u003C/p>\u003Cp>I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations.\u003C/p>\u003Cp>We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that.\u003C/p>\u003Cp>Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows.\u003C/p>\u003Cp>This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow.\u003C/p>\u003Cp>We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow.\u003C/p>\u003Cp>In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create.\u003C/p>\u003Cp>Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage.\u003C/p>\u003Cp>We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great.\u003C/p>\u003Cp>If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally.\u003C/p>\u003Cp>I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user.\u003C/p>\u003Cp>We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there.\u003C/p>\u003Cp>We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox.\u003C/p>\u003Cp>We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here.\u003C/p>\u003Cp>For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload.\u003C/p>\u003Cp>Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user.\u003C/p>\u003Cp>Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be.\u003C/p>\u003Cp>We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you.\u003C/p>\u003Cp>And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it.\u003C/p>\u003Cp>And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications.\u003C/p>\u003Cp>Send notification. Cool. Send notification. The find_user.id. Cool.\u003C/p>\u003Cp>Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no.\u003C/p>\u003Cp>Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there.\u003C/p>\u003Cp>That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us.\u003C/p>\u003Cp>Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage.\u003C/p>\u003Cp>Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal.\u003C/p>\u003Cp>It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens.\u003C/p>\u003Cp>Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name.\u003C/p>\u003Cp>Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great.\u003C/p>\u003Cp>So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome.\u003C/p>\u003Cp>Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways.\u003C/p>\u003Cp>Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win.\u003C/p>\u003Cp>That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.\u003C/p>","Hi. Welcome back to another episode of 100 apps. 100 hours where we build some of your favorite apps or try to rebuild some of your favorite apps in 1 hour or less, or get publicly humiliated trying. Alright. Super excited to be back for another season. If you are new to 100 apps 100 hours, there are 2 basic rules. Number 1, you have 60 minutes to build and plan and build, no more, no less. So when that clock strikes 0, that's it. And then the second rule is there are no other rules. Use whatever you have at your disposal to complete the functionality. That's it. Let's dive into this episode. So today, I've got a custom CRM. Tools like Salesforce, Pipedrive, HubSpot, they really need no introduction. Everybody needs a CRM. A lot of them, depending on your purposes, may be way too much for what you need or they may not be specific enough for your industry. So we are going to build our own custom CRM in 1 hour or less. Let's do it baby. Alright. So, let's start the clock and away we go. So, when we plan our CRM, what do we need out of a CRM? Right? What kind of functionality do we want to see inside our CRM? So basically, we want to manage all of our contacts. We want to manage all the different organizations those contacts belong to. What else? We wanna manage our sales pipeline. Manage sales pipeline. That's gonna be deals or activities. We want to be able to track activities and follow-up. So this seems like a a pretty good set of functionality for a basic CRM. I'm sure we might embellish this a little bit depending on how far we get, but let's dive into actually fleshing out what the data model just might look like for something like this. So we'll drag a nice square up here. We're gonna have contacts. We could just call those people. I'm a big fan of that. We're gonna have organizations. There's definitely gonna be a relationship between those 2, but I I feel like this could be a many to many relationship because one contact could belong to multiple organizations, and and some CRMs make that a little more difficult to do than others. What else do we have? We're gonna have deals or opportunities. Deals is kind of a a standard nomenclature. We're gonna have activities that are attached to what, those are attached to organizations, they're attached to contacts, they're probably also gonna be attached to deals. What else are we going to have? We're probably going to have some sales reps, those are going to be our users inside our accounts. This looks pretty good for a a base set of functionality. Let's dive in and actually start building something. So, I'm just gonna pull up my Directus instance, that's what we're using on the back end. Totally blank instance, we'll zoom in on this, and let's start building. Right? Let's create our first collection. Let's start with contacts. So we're gonna give this a contacts as the name. For the primary key, let's use generated UUID. What kind of fields do we wanna add for our contacts? Right? When we think of contacts, we're gonna need name, email, but we may also wanna track when this was updated, when it was created. Directus makes that super easy. And one tip that you may not have realized if you've used Directus before, is I can go in and change these to be whatever I want. So if I wanted this to be created at and created by, I could go in and update those, change them to be whatever I want. Updated at, updated by. That's the the nomenclature that you're used to. Do we actually need a sort for our contacts? I don't think. Let's just go ahead and save this. Alright. So for our contacts, we're gonna add a first name. Great. We'll make that a string. Let's do half width for that. I can go in and click the three little dots here and just quickly duplicate this field to save myself some time. And, of course, as I am plotting away on this, Directus underneath is mirroring all these changes to my database schema. So we've got our first name, we've got our last name, we're gonna need a email or an email. Great. Let's require this value. And, you know, I could even go as far as, like, making this unique if I I wanted to have some type of unique identifier to match these up with. Alright. As far as an interface, you know, I could get fancy with this and we could add a little email icon like an at symbol. That's great. I think everything else is good. You know, one of the other nice things that's built into Directus is I can go in and add my own custom reg x validation. So, you know, I can put in some type of reg x that matches email addresses. I don't have one of those handy here, but we'll go ahead and keep marching along here. So we got first name, we got last name, we got email. We'll probably have a phone number at some point, but let's let's go for, like, a job title, probably. I'm trying to think of all the standard fields. Maybe we have some notes. We could set that to be a text area field. I don't really need formatting for those. Great. Job title. And maybe honestly, let let's strip this out because I'm I'm thinking about that many to many relationship with organizations, and we may have, different titles at different organizations. So I'll show you how we can handle that coming up. Alright. So we got first name, we got email. We could go ahead and add phone number as a string. Phone or phone number. Let's just keep it short. We'll say phone. Let's give it a phone icon. Great. K. Phone, email. We'll put email above phone. Cool. Looks nice. Alright. Let's just take a look. Right? We've got first name. Go ahead. Oh, I was gonna add my wife here, but she's gonna get mad at me if I misspell it. Right? Ashley atexample.com. I had a phone number, 555-5555. Here's some nice notes for our contact. These are looking great. Right? We've got our contact in here. I could potentially sort and filter this a a hundred different ways, maybe change the layout. But let's dive into the next collection that we wanna set up, our organizations. So we'll have organizations, again, for the primary key, I'm gonna use ID and just use generated UUID as the type. And, again, I could change created at I could change this structure for these system fields to be whatever I want. Created by, date updated, that's gonna be updated at, if I can actually spell. And we use this one as well and call it updated by. Great. Okay. So now we've got an organization. For our organization, what are we gonna have? We're gonna have a name of the organization. Right? Probably, some addresses for that organization. Right? We could have multiple. So that's where we might reach into an another bag of just a different table. What else are we gonna have for an organization? You know, let's just do something like country in case we want to, even, like, automatically route new organizations to a specific sales rep. Potentially, we've got a name. What are we gonna have? We're gonna have a website. You know, we may have a a logo if we wanna track that for a company. So let's do an image file. Let's call it logo. You know, you might even add things like brand color, things like that if if you're really in tracking that. And then we'll just add another section for notes. This is gonna be a text area field with the type text, and we'll hit save. So we've got our organizations. We've got our contacts. Let's make a link between the 2. Right? So how do we go about that? Depends on how you want the data to actually be structured. Right? In an application, depending on the setup, maybe a user can only belong to one specific organization. But often inside of CRM and maybe I am working with the local little league. I am a member of the board there, and then I'm also a developer advocate here at Directus or, you know, a a founder at Better Side Shop. So a lot of different relationships that is gonna be modeled with the mini to mini relationship inside Directus. Here's how we set that up. It's gonna be pretty easy. We'll just go to create field. We'll look for a many to many relationship. And because we're on organizations, our key here is gonna be contact for the or contacts, I should say. Our related collection is gonna be contacts and then we'll just go through and paint by numbers here. Do we want to show these in a list or a table? I'm good either way. We definitely want to show a link to that item so we can get to that specific contact, but I'm gonna pop open this advanced field creation mode just to show you that you do have control over the naming of the junction collection and the individual fields within that. So, the default setup here is just going to take this table name and this table name or this collection name and this collection name and marry the 2 together and we end up with organizations underscore contacts. Works for me. We'll leave that autofill set up. And then on the reverse, I'm gonna add that many to many field to the contacts as well. We're gonna keep that as the organizations. Alright? In this case, we may have a sort field. So we wanna control the the sorting for contacts, like who's 1st, 2nd, 3rd, that could be helpful in, like, a primary contact situation. And then we have some of our relational triggers. Right? On deselect of organizations' contacts, what do we want to do here? Yeah. Maybe I want to delete that association. So I'll set these to cascade. Great. So now that back out, we can see we've got our contacts field here. If we go into, yep, looks like we've got, an extra contact table created. I might have typed something wrong. But, so we can see contacts there in the organizations. And then on our contact, I should see organizations here. Let's just add an organization. Right? So let's say my wife worked at Tesla. That's in the United States. And, you know, we could add the website, logo, notes. I could choose existing contacts. But I'm just going to go ahead and say 1. Right? So now, I could see I've got that organization here, but it's showing an ID, which is not super helpful. Right? So I can go into the organization itself and control the display template where I have a name. I could even potentially add the logo to that if I wanted. So now if we check it again, we can see, okay, here's the organizations that they're a part of. Right? Now, you also have the ability to manage the data inside that junction collection. Right? So if I go in and and like I said, I may have a different job title at all these different organizations. So I could go in and add a new field here inside this junction collection for job title. Great. Alright. So, now if we go into Tesla, we can see the job title is down here at the bottom, but Directus also gives me an easy way to control where that displays. So if I go to our many to many relationship inside our contacts, I can go to maybe we wanna add a sort field for this just to make sure. And then if I go to the interface, I can control where my junction fields are located. So I can put this at the top, which should be great. Okay. So now if we just check that out one more time. Right. Now I could see director of DirectUs. So now I could give a specific job title to this specific person within that organization. I could go in and add a new organization as well, like, hey, the little league. We can say a board member. Right? Little League Baseball is the name of the organization. Great. Okay. So now there my wife is a part of 2 organizations, different job titles for each. Right? Alright. How are we looking on time? We're looking great. Let's move on to our next collection that we want to set up. That's gonna be what? Our deals, our activities, what do we want to set up next? Let's go for deals or opportunities. Deals is probably the standard naming for this. So that's what we'll stick with. Could be opportunities. Could be something else. That's fine. We'll do created at. Just to keep the same structure, we'll do created by. And by adding these, whenever this record is created, it's going to populate a timestamp and a user. The same thing for Updated, whenever it gets updated or who updates it, it's gonna populate that info for us. Updated by. Great. Okay. So our deals what we're gonna have for our deal? We'll probably have a name or a title of the deal. Deal name sounds great. It could be good to prefix some of these sometimes if you're dealing with a lot of nested relational data. So you get used to seeing name in there a 100 times. Could be confusing whether that's the actual deal name or the organization name or the contact name. So we'll just save the deal name. What else are we gonna have on this deal? Right? We'll probably have a average dollar amount or a potential dollar amount. Let's set that to be, decimal. Deal value. We can add a nice little dollar sign to the icon to make this look nice and pretty. And then we are going to have some related fields. But before we do that, let's just add, like, some deal notes or something like that. Sounds great. Okay. So we got a deal name, we got a deal value, you know, maybe these are side by side, not a big deal. Let's go in, and now we want to add a relationship to the organization and probably to our primary contact for this deal. So those are both gonna be many to one relationships. And we'll set a key of organization here. So the related collection, we'll set that to be organizations. Great. And I could control the display template here. We'll just use the name and we hit save. So now we've got an organization that we're gonna tie to the deal. I'll make that half width. And then we also want to add a primary contact. So we'll just say primary contact. The related collection here is going to be the Contacts collection. And for the Display template, let's use first name, last name, and let's use this format, which I think is typically how it displays inside an email client where you have the email address inside these less than or greater than SQL or symbols. So let's save that. We've got our primary contact. We've got our notes. We've got our deal. What do we need next? Right? We need to track where that deal is at. So we could use, like, a a status field as, like, a drop down for that potentially. Right? What's the status of this particular deal? Blah blah blah. Or we could do something else where we have a relationship to a deal stage or our individual pipeline. Right? Because then we may want to, potentially have separate pipelines or we we wanna give more control back to our users, so the the sales reps or the sales manager to control that actual pipeline without getting into the admin section. So how can we do that? Right? Looks like I've got a little extra couple of fields that I've been creating here inadvertently. Alright, so now let's add a new collection. Let's just call it Deal Stages. We want to manage what are the stages of a particular deal. As far as the optional fields, maybe I don't really not super concerned with these, deal stages. We're gonna call this the name of this particular stage. Maybe we wanna give it a color so we can add some color to it. And, you know, I guess we could even give it an icon if we wanted to. We could play around with that and see what that looks like. Deal stages. Let's make both of those half width. And, now, let's just go through and map some of these particular stages. Right? And I and I, you know, I've got this s on here. One of the other things that you could do inside Directus, you can't control the translations for all of these. So even if you're working in English, on the the back end, you may want to use prefix tables to keep things nice and organized as a developer. This is a great way to control what displays via the interface to your actual users within the application. So I could just call this deal stages or pipeline stages, whatever makes a lot of sense here, and hit save. Alright. So it still shows me deals_stages here, but when I start looking inside the actual app, there we go, we can see our pipeline stages. So, let's create our first one. Right? Let's just say, New, this is inbound. Do we have a symbol? We do have a symbol for this. Right? So let's say red. This is we need to take action on this. What's next? Right? Qualified, maybe? Contacted? Or, let's say assigned. Right? We forgot to assign those to a sales rep. Assign assignment. That's a nice looking icon for that. Then we'll say qualified. You know, if we're doing software sales, like, a check mark. Okay. What else? Demo. Let's save green. Oh, let's make this orange. Do we have like a demo? Maybe a TV? What have we got? Yeah, there we go. That works for a demo as far as the icons. And then we are going to set, like, a last stage, like, proposal. Great. Let's just go gray. Cool. Document. Awesome. Alright. So I can control the way that these are displayed through my actual settings, here. So I can go into each specific one. And on the display tab, let's display a colored dot for the color. And then for the icon, we're just gonna display the actual icon. Alright. Great. If we take a look now, I can see what these actually look like. Cool. And then for my display template for the deal stages, I may even go in and just mirror that same structure where I have a color, then I have an icon with a name. Okay. So, now, what we've done, we've effectively given control over these, and we probably actually need a sort on these as well, so so we can control that value. So, let's go in and quickly add a sort. That's going to be an integer value. And because this is coming after the fact, after we've created this actual table, I'm just gonna scroll down a little bit, and I will have a sort field here. So we'll choose sort just to make sure we manage that. And now I should be able to drag and drop these, in whatever order that I like. Again, we're giving control back to the users of our custom CRM. We're definitely gonna prevent them from getting inside the admin so they don't mess with the data model. But here, we give them the ability to control what those pipeline stages are gonna be. So now we need to add those to our actual deals. Right? So we're gonna go into our deals. We're going to create a many to one relationship. And, you know, this is going to be the deal stage or just stage. And we're gonna use deals underscore stages as the related collection. We can open this up and and just see what's going on. I I don't really need to create that inverse relationship. I could. Doesn't make a ton of sense, though. I'm not sure. Alright. Display related values, validation. Let's keep it very simple, and then we're gonna use our deal stage. Alright. So now if I go into our deals Why is this oh, it goes in the machine. I don't know what's going on on this particular example, but it keeps creating additional fields that we don't need. Alright. So let's create a new deal. Alright. This is gonna be a new deal. We can see, hey, there's our stage. That looks great. The deal name is 100 apps, 100 hours contract. It's worth $1,000,000. So we'll add that. How many zeros do we have there? 1, 2, 3, 4, 5, 6. Alright. Let's make this for the Little League team. Great. My wife is the primary contact. Here's some notes on this deal. Great. Did we save it? Numeric value and deals is out of range. Uh-oh. Wouldn't be a 100 apps, a 100 hours without, some type of issue. Right? So let's take a look at this. The deal value, what did I set? Precision and scale. Maybe we bumped this up way up. Probably could have gone with integers for this as well. Do I have a minimum and a maximum? I I don't have that set. So not entirely sure what's going on. Let's just try this again. 100 apps, 100 hours contract, 1 100,000, 1,000,000. Try it again. Save. Okay. Alright. So now, we've got this particular deal set up. This is kind of an underwhelming view. Right? And, again, I might control how these things display. So if I just go into our deals, we go to the deal stage. If I wanna control how it displays, I can go in and set this. So let's do the color, do the icon, do the name, just because, I'm a I'm a very visual person, and I suspect, a lot of your end users may be as well. We still don't see the organization there. So, again, we'll go back. We'll adjust our organization. We wanna display related values. We want to show the name of that specific organization. And let's get even fancier and maybe we wanna show that logo. I'm gonna click on this and then you'll see this one that has a little magic beside it that's a thumbnail. That's just a shortcut for, like, a nice thumbnail sized image instead of loading the actual image size. So I donit have a logo for this company, but I could quickly find 1. Alright. So, we'll just copy this image address, go back to our instance, let's load up that specific company, they who shall not be named, Hit import. Oh, that's a data URL. Let's find an actual URL that we can just copy. Great. Let's try this again. We can import via URL. Cool. And now when I'm looking at my deals, I can see the logo of that organization. That makes it really handy, when I'm working on a deal just to have that extra extra visual reinforcement. Now, when I'm looking at deals, I may want to set up like a traditional pipeline type of view. So I could do that really easily just by switching the layout here inside Directus. So we'll change this to a Kanban layout. Let's group by the deal stage and the group title is gonna be the name. So I can see new assigned qualified demo. We have that Kanban view that we're used to. As far as the text, what do we wanna look at? We want to control, let's say, when this was created. I'm not really concerned about that. The tags, what do we want to set that to be? Do we want a card image? Honestly, this looks okay. Do we need an actual user on here? Probably not. Cool. Alright. We'll just keep it as is and let's let's start fleshing this out a little more as well. Right? So within a deal, when I'm working this deal, we're going to have activities assigned to this as it moves across the pipeline. And, for that, let's just go in and add a new table, a new collection, we're going to call it activities. Activities, I think that's the correct spelling. We use generated UID, we'll do created at, createdby, create up, updatedat, updated by. Great. And, in this case, maybe we do give a status for this particular activity. Right. Status, great. Let's give a name of the activity, probably a type of activity. Makes sense. We use a drop down for that. It seems pretty straightforward or, you know, maybe they're not gonna need to change the activity types on a a very frequent basis. So we'll call it the activity type, and let's add a couple choices to this. So this is a relatively new addition to Directus within the drop down, the ability to have an icon and a color. So let's say text is a phone call, value is a phone. One of the other cool things if you want to translate this value, you could use this phone t, or this dollar sign t for a translatable string. And then anytime you have users who are using the app within a different language, inside the settings, you can control all those custom translations. We'll take a look at that in a moment. Let's call this what? Phone call. The value could just be phone call, depending on how I wanna store this. Right? It may have an underscore within it as well. It could just be the same thing. Either way. Look for that phone. Great. And I could even add a color for that if needed. Maybe I just wanna keep these all the same. Phone call, we'll call it a meeting. And for this, do we have a meeting icon? Right? We calendar looks nice. Maybe we want to track demos separate from meetings. Alright. So we drag a demo. That was the TV icon that we had. Great. What else are we gonna need? Like, an email? Not sure I would Yeah. Maybe, like, a follow-up email. We wanna reschedule that. And then we'll do an email. Great. Demo, phone call. I think I'm gonna set this to be underscore value. Cool. Alright. Looking good. Activity type. Okay. What else are we going to need for this? We need a due date for this activity and the status, we can use as as is. It's published draft, archived. What do I really care about this activity? It probably isn't completed or not. Right? So maybe we scrap status, and maybe we just go for a toggle instead. Right? Hey. Is this completed? The default value, great. For our label, maybe we change it to completed and give it a color of color on. Color off is red, just to show that it has not been completed. Great. And let's just clean up this form a little bit, making sure everything looks nice. Okay. We are going to add a date for this, and let's set a specific date and time that this thing occurred. Call it due date, end date. Due date seems reasonable, but when do we actually need to complete this specific task? Okay. So now we've got an activity. We want to, link this to our actual deal so we can track those. Right? So, what I'm gonna do in this case is create a mini to one relationship because this activity could only belong to a single deal. Right? We're gonna use the key of deal. The related collection, we'll set that to deals. We'll show the name of that deal. Maybe we show the actual organization as well so I can actually dig into the related collections and and show values from that? Excuse me one second. Sorry about that. Cool. Alright. So now, what I forgot to do is create that inverse relationship. You can actually set that up via direct us when you're creating that relationship. But now I can also just go into our deals, not deal. We'll go into our deals, and now we're gonna create a one to many relationship back to those activities. So we'll call this key of activities. We've got activities. The foreign key will be deal. That already exists. Maybe we wanna show these in a table. We'll choose the columns, due date, Name, Due Date, Activity Type. Seems pretty savvy. And I can even filter these, right? So if I wanted to see just activities where they were not completed. We can enable search and filtering and show a link to these. We'll take a look at what all these look like in just a moment. But we've forgotten one important step through this whole process is, hey. We need somebody to assign these deals to. Right? So, let's add a many to one relationship. We'll call that the deal owner or, you know, you potentially say who this is assigned to. Like, the deal owners, again, kind of standard naming in these scenarios. And for the related collection here, we're gonna use directus underscore users. So these are gonna be actual users of the application that we're assigning this to. Invalid payload collections can't start with direct us users. Oh, deal owner. Let's go to our related collection, and let's get direct us underscore users. And, in this case, we're gonna show the first name, last name. We may back up and do an avatar as well. So just the the thumbnail, the avatar, I could move these around just by using edit raw value. You'll see these are just, the standard mustache syntax that you see throughout Directus as well. Excuse me. Let me get this a drink of water. I'm actually dying. That's a turn of fate. Okay. Alright. So we're gonna save this. Deal owner already exists in deals. Okay. Alright. Great. Let's move this around. Maybe we make deal stage half width. We slide deal owner up there. Let's actually take a look at this now. I should be able to assign folks. So let's create a new user. We'll just call it sales rep. Salesrep@example.com. And maybe we give them a nice avatar. Right? Sales rep, avatar. Let's just see what Google comes back with. John's inside sales rep. Yeah. This looks this is perfect. This is my guy right here. Alright. There's his avatar. We'll just save that. And now we can see who we've assigned this particular deal to. And now maybe within the deal card user here, I wanna show who that deal owner is. Great. Mister sales rep. Looking good. Alright. One of the other things that we need to do on our activities, we probably got a an owner of that activity or assigned to. It's been assigned to somebody on the team. Again, that's going to be assigned to a direct us user. We'll save that. And lots going on here. Just some type of weird glitch. I could see a couple of extra collections. I'll just remove these. Alright. Cool. So now weive got a deal. Weive got a table full of activities. I can go ahead and add these, like, follow-up on proposal. This is assigned to Mr. Sales Rep. We've got the activity type. We can see that's going to be, just a quick phone call. This is completed. We can see that conditional, conditional formatting for that. And we can add a due date of, let's say, the next Friday. Great. Save that. Keep editing. Cool. Alright. So, now, we've got the basic inner workings of a CRM. Right? We've got our deals, we've got contacts, we've got organizations, we've got our different pipeline stages. Right? If I wanted to organize these things a bit, we could go in and add different icons for each of these. So, you know, maybe we set some people icons for our contacts. We've got our organizations. Do we have an organization option? Let's look and see. Business. Is there a business? There you go. That looks somewhat like a business. We've got activities. Maybe this will be like a checklist. Cool. We've got our deals. Let's make those the money. Dollar signs, that's great. And then, deal stages, to me this is like a settings. Right? So I could create a new folder, let's just call it settings folder. You don't necessarily have to add this, but maybe we just do to keep it clean. And we look for, like, a settings icon just to use here. That's great. This one looks magical. Settings suggest. Right? And, again, I can change the name of this to just say settings. So, it still creates this collection, but, we can call it whatever we want. So we'll drag this up within settings folder. We'll drag deal stages and, this will be, what, like the Kanban view. There we go. Awesome. Okay. So now we get a little more organization to this. One of the other things that you might do and, you know, that you use all the time within ACRM are saved views. Right? So Directus gives you that ability with bookmarks. We'll just go in to the top here and maybe I want to sort by a specific sales rep. Right? Like, the deal owner is, a specific person and specific name is sales rep. That's the only one at this point. But I could go in and create a bookmark for this, and we could call it, deals sales rep Man. And we can change this up, give it a color. Now, within that collection, now we can see we've got our deals for Sales Rep Van, and I could save that bookmark. So even if I go into the main deals view, and maybe we change this back to a table view, which could be easier for, you know, maybe a sales manager or something who's controlling this. So I had deal owner back to this as well. Now if I go back, deal sales rep man view, boom, there it is. It's saved. I could go in and update this if I wanted to as well. So now we've got our pipeline. When we go into each one of the deal, we have our name, we've got our organization, we've got the notes, we've got our activities. You know, we can mark these activities off as completed. That seems like a great CRM baseline. Let's let's take a look at where we're at. Right? We got, like, 16 minutes on the clock. This feels like a win. I don't I don't know if we wanna run that one, let's discuss where we could go from here, right, maybe we want to automatically send some emails when it hits a certain stage in the pipeline. So let's just call these things done. Right? We can manage all of our contacts. We can manage all of our organizations. We can manage our sales pipelines. We can track our activities and follow-up. So let's say, you know, we get a new deal inbound. Maybe we want to automatically assign that to a a particular person, or we want to send a notification to our sales rep when that assignment happens. Let's figure out how to do that. Right? So I'm gonna go in. Let's just create a new deal. We'll say actually, let's wait a moment. Let's go into our flows. This is a good example. Right? Whenever a a new deal comes inbound, we want to send an email notification to our sales rep to to let them know. Alright. So we're gonna create a new flow. We'll just call it new inbound deal. Pretty straightforward. We could change this to the new symbol if we want to and just do a trigger setup. So what are we going to choose here? Directus gives you a ton of different options, as far as what to use when you're creating a flow. In this case, we're going to use the event hook. So when a certain event happens inside the platform, we wanna trigger an automation. The type that we're gonna choose here is action non blocking because we don't, the the filter allows you to basically either adjust the payload when a new deal gets created or a new event happens. Action non blocking, again, that runs after a create or update action. So in this case, let's do the items dot create. Whenever we're gonna trigger this based on the deals collection. And cool. Alright, so now I'm just going to save this, right? I'm going to go in and let's create a new deal inside the system. It's in the new stage. We're gonna assign this to mister sales rep. New deal automation. And we add, let's set this one to be for the little league. Again, we choose a specific contact, add some notes, and, maybe, the deal value, we'll just ballpark it at $5,000. Great. If I go back, now I can see that in my logs, I've got this flow. Here's the payload of this particular flow, and we could see who the deal owner is on this particular deal. How do we send an email notification to that specific owner? Right? I'm just gonna take this, copy it, and I open up just my Versus code editor where I've got my 100 apps, just, Docker Compose file here set up to run this locally. I'm just going to save that in case I need it. And, next, let's flesh this out a little bit. Right? So you can see the data we're getting back here. We're probably gonna need to look up that specific user. We can find them there. Those are the deal owner. We could send them a notification inside the app or we could send them a notification via email. Right? There's 2 options there. We've got notification. In this case, we've got the UUID of the user that we're going to send that to. So, you know, potentially, you want to send that in app. App. In this case, if there's a new deal, they're probably gonna be in their inbox. We're going to send that new deal to them. But let's actually find that email address first, though. Alright. So we're gonna read data from a specific collection. Let's call it Find User is the actual step we're gonna do here. For the permissions, let's just give full access. And, for the collection, you can see I don't have the Directus Users collection here, but I can go in and edit my raw value and just use Directus underscore users. And for our IDs, right, what are we gonna put here? So if I open this back up, we're gonna use the trigger. Payload. Dealowner. So we'll do this, we'll do trigger dot payload dot deal underscore owner. And I'm gonna wrap this in mustache syntax. And let's try this again. So we'll read the user. Let's go ahead and maybe just add this send email as well. So we'll send the email, and this is gonna be the read underscore user. We're using the key that we set of the previous operation within that flow. Readuser. Email should be. We'll input that. New deal assigned to you. And hey. Read_user.firstname. We've assigned a new deal to you. And then I can even go through and add those different variables if I wanted to for things like the deal name or the deal ID and and add a link back to that. So let's just try trigger. Payload.deal_name. Great. We'll save it. And now let's just test this out. I'm not actually sure if I've got emails set up here locally on this particular instance though, so that could be fun. But we should be able to actually see if this runs. In light of that bit of news, because it just looking at my configuration here, I do not have email configured here locally. So let's let's detach that one, and let's just test the notifications. Send notification. Cool. Send notification. The find_user.id. Cool. Full access. And it was a new deal assigned. We'll just add the key here. So that'll be trigger dot payload. Or no. Actually, it may be something like this where we have key. Alright. Let's just take a look just to make sure. Alright. Within our payload, we can see the key there. That's good. In that case, we probably didn't need to get the actual user there, but that's okay. I'm just gonna copy this message that we set here. Just paste that. And let's take a look at where this gets us. Alright. So we've got a new inbound deal. We're going to read the data of the user that we've assigned that to and we're going to send a notification to that specific person. Alright. Now we've got a new deal, deal stage. Let's just set it to new. Deal owner. I'm gonna choose myself here so we could test this notification. Say deal name. Test deal. It's worth $6,000,000. Very nice deal. We got a primary contact. Here's some notes. We'll just save that and let's see what happens. Right? We go to our Flow, we get a new inbound deal, we can see our logs, send notification, recipient is so and so. Hey, undefined. We've sent you a new test deal. Underscore first name. Why did that not come back? Read underscore oh, that's why I used the wrong key. Sometimes that happens. Find user dot first name. Great. So now, if I look and I check my activity log, I can see that I've got this new deal signed. Here's the notification for that. And if I click on it where it says view content, it should take me to that specific test deal. Great. Awesome. Let's call that a win. Right? We've got our custom CRM built out. We've set up some automation for this. We could go further and flesh this out a a ton of different ways. Right? If we talk about it, like, we could go through and send emails to our actual primary contact when it reaches a certain stage, that would be fairly easy to do using direct dis flows. You know, we could maybe even go as far as, like, a future future iteration of this could be setting up an inbox to parse incoming emails like a BCC functionality and add these to those specific deals as well. But this this feels really good. I'm I'm calling this a win. That's our custom CRM. Thanks for joining me on this episode of 100 apps, 100 hours. We'll catch you on the next one.","592c22ad-1cb2-4d65-9bd7-e0d7c98fe4f2",[185],"e9e66fa8-0650-4e37-ae8b-74755fdd5dca",[],{"reps":188},[189,245],{"name":190,"sdr":8,"link":191,"countries":192,"states":194},"John Daniels","https://meet.directus.io/meetings/john2144/john-contact-form-meeting",[193],"United States",[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,243,244],"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":246,"link":247,"countries":248},"Michelle Riber","https://meetings.hubspot.com/mriber",[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,435,436,226,437,438],"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",1773850429827]