[{"data":1,"prerenderedAt":439},["ShallowReactive",2],{"footer-primary":3,"footer-secondary":93,"footer-description":119,"100-apps-100-hours-community-platform":121,"100-apps-100-hours-community-platform-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},"8fed0eee-43f6-4767-8b77-79da7a059821","community-platform","896091486","Every thriving community needs a home. Dive right in alongside Bryant and he builds a community platform inspired by Circle. He races against the clock to build an engaging and interactive community space in just one hour.","903b4fe1-bb22-4e2f-b946-7fce5cff7a11",66,[129],{"name":130,"url":131},"Bryant Gillespie","https://directus.io/team/bryant-gillespie",7,"2024-01-22","Mission: Community Platform","\u003Cp>Speaker 0: Hi guys. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus. And today, we will be building a community platform. Now, this is one that is near and dear to my heart because I have built a community for sign and print shop owners myself.\u003C/p>\u003Cp>We use Facebook Groups for that. Not the best community platform, but it is free and everybody already has a Facebook account. But in just a quick look at the research for this episode, There's quite a few different tools out there that I located, Circle being one of the more popular ones. It's not exactly a forum, it's not exactly a course, it's not exactly chat, it's all of those things. So the pieces that we're really going to dive into today are this concept that they have of spaces.\u003C/p>\u003Cp>So each one of these items in the left navigation is a space. A space can be, either private or public. So I can see here some of the circle experts and this is their their own example community. Right? They do have courses in here.\u003C/p>\u003Cp>We've already done an episode on LMS, so we won't really touch on that. But, if we get into this, we could see what's new in the product. So this is an update we're gonna post. We've got some comments down at the bottom, I'm assuming, and you had to be logged in to comment. So how do we kinda recreate some of this functionality?\u003C/p>\u003Cp>It looks like they have weekly events here, but we're probably not seeing those. And maybe here's something similar. So anyway, that's what we're building. If you, have watched any of the other episodes, you probably already know the rules. We've got 60 minutes to plan and build, no more, no less.\u003C/p>\u003Cp>And the second rule, which is not really a rule, is use whatever you have at your disposal. So, any of the tools that are out there, AI, Tailwind CSS is one of my favorite ones. We're going to be building this platform on Directus as well. That'll be our back end. You could probably even actually build the entire thing inside Directus, but we'll probably go for a custom UI on the front end.\u003C/p>\u003Cp>Alright. So, sounds great. Let's rock and roll. So we'll start the 60 minute timer and away we go. So anytime I build an app, I always love to just kinda flesh out the functionality ahead of time.\u003C/p>\u003Cp>So if we're looking at this specific app, maybe I'll just pull up a text box inside Figma here. Here's the functionality that we really want out of this app. Right? We want to create and manage different spaces. Spaces for a how do I can I constrain this?\u003C/p>\u003Cp>Yeah. There we go. For a community. Maybe we have events as well that we're going to list. List and create and manage events.\u003C/p>\u003Cp>Events. We need to be able to log in. Users can log in. Create posts. Right?\u003C/p>\u003Cp>Is that what we're going to call that? Maybe a thread? We'll get into it. Right? We'll create some posts and add comments to those posts.\u003C/p>\u003Cp>That feels like a a pretty good chunk of functionality. I don't know if we'll get to all that or not. That's, like, actually building the UI for that, could be fun. Right? Alright.\u003C/p>\u003Cp>So let's just, like, flesh out our our actual data model here as well. So again, I'm not the savviest Figma user, but I try. I'm a tried. Alright. So what do we have?\u003C/p>\u003Cp>We've got spaces. So those are gonna be our little homes for all this different stuff, this different type of content. Right? Then we've probably got, this is where we get into the naming. Right?\u003C/p>\u003Cp>Is it a a thread? Is this a what what are these things called? Are these posts? Yeah. Let's go with that.\u003C/p>\u003Cp>Right? This is a post. And a post has some comments. Probably has a probably has, like, a post type, I'm assuming. K.\u003C/p>\u003Cp>Let's look at what else we've got here. Share your wins. Log in. Yeah. Okay.\u003C/p>\u003Cp>So these are different posts. You can upload images to a post. You can add comments. You can like. So we probably got some likes in there.\u003C/p>\u003Cp>And I don't know that we'll get to that either. But, what else do we have? We have users. That's gonna be taken care of by Directus because it gives us that out of the box, which is nice. That'll be our Directus users collection.\u003C/p>\u003Cp>And what else do we need for a function community? Right. Are events gonna be a separate thing than posts? I don't know. Let's we'll try it out.\u003C/p>\u003Cp>Cool. Let's just add that to the end here in case we we don't have time to get to it. Alright. So this feels pretty good. We're closing in on 4 minutes or so in the planning stage.\u003C/p>\u003Cp>Alright. So what do we have set up? We have, we got Directus going here. I've got a Directus instance that is totally blank, so I just reload. You'll see there are 0 collections.\u003C/p>\u003Cp>And on the front end, I've got a starter Nuxt application that just basically has the Directus module or the Directus plug in already preconfigured. We're using the SDK to communicate with the Directus instance, but I've just got Directus spun up on a local Docker instance. So, really fancy app here. We've got Tailwind. We're using Nuxt UI as well on that front end.\u003C/p>\u003Cp>So got quite a few things going on there. Alright. So first and foremost, we've got spaces. Let's go ahead and create our data model. Directus makes this super easy.\u003C/p>\u003Cp>We'll just go into our blank instance. We'll create our first collection. We're going to call it spaces. So those are the different spaces here. We'll generate the UID.\u003C/p>\u003Cp>We'll probably have a a status for the space. What's the sort order for the spaces? Who created these? When were they created? That's all pretty standard.\u003C/p>\u003Cp>Let's just take a look at the network requests on this to see if we can actually pick up some of their data model. Alright. Space groups. Alright. There's the circle community.\u003C/p>\u003Cp>Allow members to spaces. What else do we have? There's a slug for the space. So that's, that's a handy piece of information to have. Spaces, discussion.\u003C/p>\u003Cp>Okay. So we've got a cover image. Cover image URL. And this is via their API, so it doesn't necessarily represent the underlying database structure, but this data is coming from somewhere. So we're gonna have a name for the space.\u003C/p>\u003Cp>Name for the space. That's great. Alright. Do they have a description? Space description.\u003C/p>\u003Cp>Space name. We're definitely gonna have a slug, so that's great. We'll roll with that. Advanced field creation mode and, when I need something like a slug or a value that URL safe, if I go into advanced mode, I have Slugify available. So that will automatically transform the input as I type.\u003C/p>\u003Cp>Lock screen heading. I don't know, man. Let's just give it a description. And let's keep that non WYSIWYG. And last but not least, for our space, spaces, Let's add a cover image.\u003C/p>\u003Cp>So we've got our cover image. Right? And let's see if they had do they have a type of space? Space, slug. Visibility, post type is basic.\u003C/p>\u003Cp>I I wanna say there's probably some setting in here to control what these things look like, whether this is like a discussion or this is an event or a course. As we're fetching these, okay, that's a course. Internal spaces API. Maybe a type. Is there a type?\u003C/p>\u003Cp>Course type. Type is a banner. K. Those are yeah. No.\u003C/p>\u003Cp>I I don't wanna waste time on it. Right? Cool. So status, we'll just leave blank for now. Is this published or is it available?\u003C/p>\u003Cp>And then let's say let's add 1, like, is private. This will be a toggle. So the Boolean values Boolean value, I always used to struggle with that. That's what we'll save in the database. Is this space private or not?\u003C/p>\u003Cp>And then we can do a little cleanup here on our fields. So name, slug, description, maybe we pop that right beside there. And status wise, let's just roll with it. Right? So we go in, we create our first space.\u003C/p>\u003Cp>Maybe we wanna call this discussion, and that'll be at the slug for that will be discussion. Here's where we discuss stuff. Great. Is it private? Yes.\u003C/p>\u003Cp>Let's make it private. And then we will let's upload a cover image. Right? Unsplash. Let's just search for discussion.\u003C/p>\u003Cp>Okay. Images that are not seem to be working. Okay. Maybe some type of Internet issue. We're working here locally, anyway.\u003C/p>\u003Cp>Let's just grab from Circle. Right? We're already working with this specific thing. Do they have discussion? Where's the best practices for engagement?\u003C/p>\u003Cp>Yeah. Fine. Let's roll with it. Cool. Alright.\u003C/p>\u003Cp>So now we've got our first space. What else do we need to add to this? Right? So we've got posts for the spaces. Then each one of those posts could have comments, potentially.\u003C/p>\u003Cp>Right? So, let's just go in and create another. We'll just call it announcements as the next space. Announcements, read only channel for important announcements. Very fancy.\u003C/p>\u003Cp>Important announcements. Alright. So that is not private. Let's make both of these published just to to have it. Great.\u003C/p>\u003Cp>Okay, so we'll go back into our settings and let's add the next item. Let's add posts. We use the UUID. Do we have a status for the post? Yeah.\u003C/p>\u003Cp>Probably. Date created, date updated, is there a sort for the post? Do we wanna control the sort order on those? Yeah. Maybe potentially you do.\u003C/p>\u003Cp>Alright. So if we take a look at circle here. Let's dissect one of these posts. Right? So a post has a title, looks like.\u003C/p>\u003Cp>Also has a user that created the post. So Directus has taken care of that for us out of the box. We probably got a date that this was created. We've got some rich text for the post. So that'll be the post content, maybe?\u003C/p>\u003Cp>Post body? You do that. Let's just keep it as content. It seems pretty straightforward. Naming stuff is always the the hardest part about development.\u003C/p>\u003Cp>What else do we have? We got that cover image for the post. Right? Cover image for our post so that, you know, if we want to upload a cover image, we can. Sweet.\u003C/p>\u003Cp>Looks great. Let's go in and just create one of those posts. Right? So now we've got our post. Let's add a published post.\u003C/p>\u003Cp>And, Kate, if you're watching this, I'm sorry. I'm just gonna steal this post entirely. Use it verbatim inside this platform. And I'm also gonna steal your thumbnail image. Don't sue me, please.\u003C/p>\u003Cp>Alright. So we've got our post. Great. Looks savvy. Okay.\u003C/p>\u003Cp>Now we need to tie these together. Right? We don't have it's just floating out in space. It's not inside a space, it's in outer space. Alright, so let's tie that together.\u003C/p>\u003Cp>We're going to use the relationship indirectus for that. We'll just go to our spaces and one space could have many posts. But I think a post always belongs to a single space. We wouldn't want posts in multiple spaces. So that is a good example of our one to many relationship.\u003C/p>\u003Cp>There's one space, many posts, and these are gonna be called posts. The foreign key inside our post table, we'll just call that space without the s, because it's just gonna be 1. Alright. So we'll save this. And I can go into advanced mode inside Directus just to to make sure everything that I want is getting created.\u003C/p>\u003Cp>And maybe we I don't know if we actually need that sort field or not, but it's fine. Let's do a post title. I probably could have went with the post name, because we had a space name and not a title. But, again, we're splitting hairs here. There's no right or wrong when when you're naming things.\u003C/p>\u003Cp>There's only shades of gray. Some things are bad, some things are not. Alright. So we've got our spaces, we've got our posts, we've got a relationship between them. Right?\u003C/p>\u003Cp>So if I go into my spaces now and I click add existing, I can add that existing post to our announcements space. I could save that. And then if I were to go to our discussion, I could potentially pull that in here, but because of that relationship, it would decouple those. Alright. So next we've got, what, comments?\u003C/p>\u003Cp>Posts could have multiple comments. And let's do date created. We really don't have a status on these, do we? Like, you know, if the post if the comment is edited, we'll we'll be able to tell by the user updated field or the date updated field. So there we go.\u003C/p>\u003Cp>We've got the comments content, content body, comment message. Yeah. Let's roll with message. That's fine. So now we've got our comments.\u003C/p>\u003Cp>Let's send that relationship to the post. So we are, again, going to do a another one to many relationship. Right? Because we have a post. One post has many con comments.\u003C/p>\u003Cp>Okay. So oh, actually, we're on comments here. Right? So we need the inverse relationship of that. We've got the many to 1, because we were on the comments.\u003C/p>\u003Cp>So the key here will be post. Our related collection is posts. I'm gonna open up advanced because I am going to add that corresponding field inside the posts collection. That's gonna be comments. Great.\u003C/p>\u003Cp>We can add our title there. Cool. Okay. So we've got comment, we've got a post. Now if we were to go, actually, let's just hop over to our post, make sure this is showing.\u003C/p>\u003Cp>Alright. Maybe we wanna show the space that this is attached to as well. Go into my post. Here's the post. There's the title.\u003C/p>\u003Cp>Here's the space. I can go in and add a comment. Really simple. New comment. Bada bing bada boom.\u003C/p>\u003Cp>Got a comment for that space. I can see it here, though we can't actually read it. It's just showing the UUID. So I can go back in and maybe we clean this up a little bit where we got our spaces, got our posts. And here in the display template for this, I could show the actual message.\u003C/p>\u003Cp>And if I wanted to, I could even get into the the weeds a bit here and show the user avatar. Is it gonna be the avatar or maybe the actual avatar thumbnail? Yeah. There we go. Looking nice.\u003C/p>\u003Cp>Cool. Let's see what that looks like. Inside our post, new comment. We don't have an avatar for for little old me, so let's solve that really quickly as well. Just do right.\u003C/p>\u003Cp>Right ahead. Where are you? Me. There we go. Cool.\u003C/p>\u003Cp>We'll go ahead. Alright. So I could see myself that I made a comment. We can see the space it's attached to. What what are we missing from our functionality?\u003C/p>\u003Cp>Right? Comments, likes, and we could do that. Post, comments, likes. I you know, do we even really wanna do likes on this? Yeah.\u003C/p>\u003Cp>I think this is fine. Right? Let's let's roll with this for now and get something happening on our front end. Let's build something cool. I'm sure I'm missing something.\u003C/p>\u003Cp>But oh, what if we had, like, threaded comets? Right? I don't know if the circle support that type of maneuver. Looks like it does. Right?\u003C/p>\u003Cp>So, I can also have recursive relationships. So this will be a good look at what's available inside Directus as well. So I could go in and create recursive relationships. Boom. Boom.\u003C/p>\u003Cp>Boom. Boom. Boom. And, you know, we we could take care of this on the front end. Or, you know, if I wanted to show that recursion, we could do it here.\u003C/p>\u003Cp>So we'll add a parent key, and then we'll just reference that same collection. And let's do, like, children. Sounds cool. Great. K.\u003C/p>\u003Cp>And as far as the interface, I could also do, like, the tree view, which would show, like, nested recursive items, as it says. But, maybe we don't drill down any more than one layer here. Cool. Alright. So now I can have a comment.\u003C/p>\u003Cp>I can have, a parent underneath that. Parent comment. Oh, did I get this backwards? Yeah. Maybe I goofed that up a little bit.\u003C/p>\u003Cp>Name these the wrong things. So children. Yeah. I did, didn't I? Let's just sort that out.\u003C/p>\u003Cp>Right? The should have been a mini to 1. Okay. So that's the parents, and that's this comments collection. And then we have our children.\u003C/p>\u003Cp>Didn't necessarily have to add that one, but cool. Okay. Now now we should be looking good. Right? So I can add all the children.\u003C/p>\u003Cp>This is a child comment. Cool. Alright. So we're setting up a lot of this. This is gonna look nice on the front end.\u003C/p>\u003Cp>Before we get into the front end, I'm also gonna go in and add a new role to the back end here. So I'll just go in, we'll call this a user. They probably don't need app access. Right? We're going to build our own front end for this.\u003C/p>\u003Cp>And then we will give them access to see all the comments, see all the posts, see all the spaces. You know, do we want them to be able to create comments? Yes. We'll let them create posts. Can they create spaces?\u003C/p>\u003Cp>No. Can they edit comments? Yes. Potentially. Can they edit posts?\u003C/p>\u003Cp>Yes. They could edit posts. But let's do a custom permission where user created has to equal their own ID. So user created equals current user dot ID, and I'm not sure if this is actually user created dot ID. Let's try it that way just to be sure.\u003C/p>\u003Cp>Alright. So now they can only edit their own posts. We could do the same for comments. And, you know, do we want to let them delete their posts? Yeah.\u003C/p>\u003Cp>Probably do. But cool. Alright. So let's check the timer. We're looking good.\u003C/p>\u003Cp>And now let's start building something. Right? This is not much of an app to look at at this point, but maybe we drag this over and start building. Cool. Alright.\u003C/p>\u003Cp>So on the index page, let's just take a look and and dissect what we've got here. We've got like a nav on the left, then we've got the actual space here on the right. And I think that is it's pretty static. Right? So let's use our Nuxt layouts for this.\u003C/p>\u003Cp>We've got a default layout. We could see that in action here. Do we wanna use layouts, or do we wanna actually do a page for this? Let's do a page. So I could use, like, a Nuxt child route for this.\u003C/p>\u003Cp>What do you call this? What do they call their URLs? Right? We load this thing up. It's just slash home.\u003C/p>\u003Cp>Right? Slash c. Okay. Oh, looks like they've got slash c. Okay.\u003C/p>\u003Cp>That's part of all of these. Right? So we we could do something similar where we have, pages a dot view vcomp ts. Yeah. I'll I'll usually end up doing something like app dot view.\u003C/p>\u003Cp>Okay. So we're gonna have 2 sections here. We've got, like, a left sidebar, and then we have the content on the right hand side. So we can, like, flex this. And on the left sidebar, They make the is the width dynamic?\u003C/p>\u003Cp>No. You can't change the width on theirs. Probably a good thing. Easier that way. So let's just say with 56, we'll do p g gray 50.\u003C/p>\u003Cp>And then we have the, what, main content. Right? Sounds great. Div. And here, we're gonna put this Nuxt page component.\u003C/p>\u003Cp>Right? Now let's see what we got. Right? If we go to pages slash a, What did I do? Why is this not working?\u003C/p>\u003Cp>Pages slash a. Come on. Refresh. Next page. Next page.\u003C/p>\u003Cp>Oh. Slasha. Okay. Slash local host/a/app. No.\u003C/p>\u003Cp>Slasha. Okay. Yeah. Cool. So we got nothing here.\u003C/p>\u003Cp>Right? Nothing here. There it is. There we go. Okay.\u003C/p>\u003Cp>So now, let's pick up our spaces. We're gonna render these over here. If I look at the Nuxt UI library, one of the things that I like here is they have this vertical navigation component. So we're just gonna use that. Right?\u003C/p>\u003Cp>Here's the structure for it. We can pull that in. Let's just drop it at the end of this. Copy these links just so we get our structure. And then we'll put vertical nav in the sidebar.\u003C/p>\u003Cp>But, obviously, you gotta delete these other ones. Okay. So we now we could see something going on. Maybe this needs to be height full. What is our default look here?\u003C/p>\u003Cp>High screen, maybe. Okay. Alright. So now we're getting that running the full gamut. We'll probably build in a little bit of padding here.\u003C/p>\u003Cp>P y 8. K. Maybe we're gonna add a logo at the top at some point. But now how do we actually get our data from Brectus into this particular instance, into this app. Alright.\u003C/p>\u003Cp>We wanna pick up these spaces dynamically. What we're gonna do in that case is call the Directus API. Alright. So we're gonna do we could use the use async data composable, especially if this is gonna be accessible on the front end, without authentication or anything. Maybe you want a server side render this if it is a space that has, you know, public data on it.\u003C/p>\u003Cp>Alright. So we're gonna use async data. We give this a key. Let's just call this, root. And I think this is the syntax.\u003C/p>\u003Cp>Alright. So I've got a composable setup in my Directus module. Where are you, mister Directus module? There we go. Use Directus, and here we will do this.\u003C/p>\u003Cp>We're going to call use Directus, and I'm gonna use the SDK that we have here. So I'm gonna read the items from our spaces collection. And, you know, I could add in, like, a some query params here or, some parameters to filter that out, but let's just take a look at our data. So I'm already seeing in the console, we've got a server 403 fetch error forbidden. Right?\u003C/p>\u003Cp>That is because we are not logged in. So how do we solve that? Right? Inside our Directus module that I've got set up, I've got a an auth composable that will log us in and fetch the user and all of that, and then store that as the user and inside use state. And I've even got in a middleware.\u003C/p>\u003Cp>Right? So we could add a middleware to this page, where if we're not logged in, it is going to send us to the authentication page. So, you know, if this was actually public data. Right? Maybe we needed to adjust our permission settings.\u003C/p>\u003Cp>Right? So in public, I could go in, maybe people can read all the comments. They could see the spaces. They'll probably still be able to read the see the spaces. Right?\u003C/p>\u003Cp>It will probably just be like the post that you would not be able to see unless they are authenticated. Alright. So here's one way we can set that up. Right? I can go into my item permissions and go to my space.\u003C/p>\u003Cp>And if this is private, if it's private, I'm going to restrict. So is private equals nothing. So if if private is null, they could see all those posts. But now if I refresh, I should remove that error. And then if I look at my app, We could drill in a couple layers here.\u003C/p>\u003Cp>Where's my page? Alright. So we can see the spaces that we're getting back here. Great. Okay.\u003C/p>\u003Cp>So we got our individual spaces. Now let's ditch these links. Alright. And just map these out. So our links are gonna be, let's say, the let's call these spaces.\u003C/p>\u003Cp>And we'll just do something like this where nav spaces equals computed. So we'll just grab this data. And we are going to map this out. Right? So we'll do spaces dot value dot map, and we got a space.\u003C/p>\u003Cp>And for each space, we are going to return what, label. So that's gonna be the space dot name. Do we have an icon for these? I we didn't have an icon, but we could potentially set one up. Or maybe we don't even have an icon.\u003C/p>\u003Cp>We'll just omit that. And then we have 2. Right? So 2 is gonna be slash, and let's use a template literal here. We'll do slash dot space spaces/space.slug.\u003C/p>\u003Cp>Cool. Alright. And now if I do something like this, let's just log this out to make sure we're getting that. Spaces. Nav.\u003C/p>\u003Cp>Why am I not getting that? Next page. Where's our app? Slash a. Where are you, baby?\u003C/p>\u003Cp>What are you doing? There it is. There's our component. Now spaces cannot read property of undefined. If spaces if no spaces maybe we do this.\u003C/p>\u003Cp>Data equals unref our spaces. No. We'll just do this. If spaces, no spaces, we'll return empty array. So let's fix our issues.\u003C/p>\u003Cp>What do we get here? So spaces is still undefined. What am I doing wrong? Rich, doing something wrong up here in the actual call. Let's take a look at the use async data.\u003C/p>\u003Cp>Oh, duh, gosh. I need more coffee. So await async data. Why is this not still need do I need to return this? There we go.\u003C/p>\u003Cp>Fingers crossed. Fingers crossed. Refresh. Okay. So we got our spaces.\u003C/p>\u003Cp>We're still getting undefined on our computer here. Unrefspaces.map. Alright. Why is nav spaces undefined? Space dot name.\u003C/p>\u003Cp>So if we look at our spaces here, we have a name. We have a slug for those. Spaces is an array. We can confirm that. Do we have it now?\u003C/p>\u003Cp>Nope. Okay. That's always the fun of doing this. Oh, nav spaces. Undefined.\u003C/p>\u003Cp>What am I doing wrong here? Good question. Let's look at the clock. 23 minutes. Alright.\u003C/p>\u003Cp>So it looks like I just forgot to return this, basically. I think that solves the problem. Okay. Yeah. So now I can actually see that.\u003C/p>\u003Cp>Right? Kind of a bonehead move, but there we go. Sometimes it happens. So now, we've got our spaces over here and we could even, let's have a look at this component. I don't know if it will actually return.\u003C/p>\u003Cp>You can use the default slot or an avatar slot, customize the avatar, or give it an icon. Yeah. What if we just give it a, like, a circle icon? Icon is circle. Let's use the material symbols, is there material symbols here?\u003C/p>\u003Cp>Maybe circle. Sorry, icon. We'll just search all these. Right? They're just like a nice circle icon we could use.\u003C/p>\u003Cp>Yeah. It's fine. This is the material circle icon. Let's try that for all of these different items. Icon.\u003C/p>\u003Cp>Does that work? No. We need to change the syntax with Nuxt UI. Yeah. It's not showing up.\u003C/p>\u003Cp>Alright. So we'll just ditch the icon for now. Cool. Alright. Let's get on with building this thing.\u003C/p>\u003Cp>Right? We've got our main page, now we've got our spaces. If I click on the space, I don't have anything showing up for the space. So we'll go in and create a new folder and then we're going to do a spaces ID dot view. Right?\u003C/p>\u003Cp>The other thing that I'm going to do here is create a new folder called a and drop spaces in there as well. And then I need to go back here and adjust this to where it says a or app or whatever it is. We're just kind of following their convention here. Alright. So we've got spaces.\u003C/p>\u003Cp>Id. We'll just create a View component and spaces. Alright. So we click here. Are we on the space?\u003C/p>\u003Cp>Yep. And we could prove that just by doing something like this where we do cons route equals use route and route. Paramsiddiscussion. Right. And we could probably call this slug.\u003C/p>\u003Cp>It might be a better way just to keep everything clean, tight. And then we refresh, we can see slug is discussion. Okay. So how do we actually use this? Again, we're just gonna go in and fetch the space and we'll use async data, and we'll do spaces.\u003C/p>\u003Cp>And, actually, do we need the we're we're getting the space slug. Yeah. So we'll probably fetch that. Let's call it spaces dash. If you don't provide a, like, key for async data, it will use the the actual print.\u003C/p>\u003Cp>It will use the the spot that it's at in the file, like, the the file and the line number and things like that. But we'll just give it an actual key here to use when it caches things. Alright. So as far as our data, we're gonna return, we're gonna use Directus, we're gonna read the items from spaces and we are going to filter this down. Right?\u003C/p>\u003Cp>So the filter is at the slug underscore equals route dot params dot slug, you know, I could filter this out a little bit, Prayms route. I don't even know if this will work. Equal slug. Returned, use directus, slug. Unexpected keyword return.\u003C/p>\u003Cp>Do I not need the return? Oh, wait. Use async data. Oh, I forgot my function there. Okay.\u003C/p>\u003Cp>So we're gonna return, use directus, and, oh, I got a period on my slug. Cool. Are we actually getting data there? Let's take a look at that component. Right?\u003C/p>\u003Cp>This is the slug, so we'll just use our view dev tools. Do we have our space? Yes. We do. And then inside this use async data call, let's back this out.\u003C/p>\u003Cp>Right? Where are you? I can do is it transform? No. They have a a set of options.\u003C/p>\u003Cp>Transform, data, data 0. Let's see what that does. Now that should grab my single space. So the spaces are space because when I read items, I'm always returning an array. But using this transform on the async data call, I can transform that data and just pick up the first item from that array.\u003C/p>\u003Cp>So there's my space. I don't see any posts for discussion, but if we go to announcements alright. Do we see any posts there? Yeah. That's great.\u003C/p>\u003Cp>Okay. So, let's take a look at what we've got here for circle. We've got our discussion or our spaces over here, and then we have the discussion here. Alright. Cool.\u003C/p>\u003Cp>So we will add a header. It's got a cover image on it, basically. Let's try to find one where we did have some discussion. Right? Courses, engagement, Contest guidelines.\u003C/p>\u003Cp>Okay. Alright. So then we click into a post, and that is the assuming that's a space. Events new. I click into that.\u003C/p>\u003Cp>Events new. That's the slug for the post, I assume. Alright. So let's render out the header image for the space. Source is gonna be space dot cover underscore image.\u003C/p>\u003Cp>We'll make this full width. Maybe a height of 32. Object cover. See what we've got here. Just flip back.\u003C/p>\u003Cp>Input must be a string. Okay. So if we don't have the space dot cover image, space dot cover image, let's not try to render it. Is that the problem? Discussion.\u003C/p>\u003Cp>Okay. So I'm trying to get the image here, but what's happening? Right? Why are we not getting the image? It is because we are getting a 403 forbidden.\u003C/p>\u003Cp>We'll go into Directus here, go back to our access control. We have not allowed files to be shared. Okay? So there's our actual item. That's great.\u003C/p>\u003Cp>This is gonna be flex Flex 1. With full. Yeah. Okay. Alright.\u003C/p>\u003Cp>So we got our header. Let's just keep moving. Then we have our post detail. Right? I've got a v text component here that this will render, like, paragraph text.\u003C/p>\u003Cp>But, if we're gonna do something like the rich text for the post, So let's look at posts. We've got, like, this post content here. So we're great for a title. Alright. So let's just do, like, a h 2 or maybe an h one for the post.\u003C/p>\u003Cp>Not sure. Let's just do h two. Oh, we're not even showing the actual post. Right? Let's do an h one because we wanna show the space name.\u003C/p>\u003Cp>Alright. So there's the space name. It gives us a little padding. Pxpy6. Okay.\u003C/p>\u003Cp>Make this bold. Make it a little larger. 2 XL. Alright. So there we have that.\u003C/p>\u003Cp>Let's dive into the actual posts. Right? So 2 ways I could go about this. I could get all of the posts in a single call with Directus, or I could fetch these separately as well. In this case, let's just render some of these posts out.\u003C/p>\u003Cp>So we will go into our query here, and we'll do fields. Alright. So if I wanted to do, like, some cards so let's just back up and take a look at circle here. Right? We've got a couple of cards here that are showing up.\u003C/p>\u003Cp>Let's do the ucard, which is inside that Nuxt UI library. We're gonna do a post in space dot post. And Get a key. Key is gonna be post dot ID. And And then let's just render out post dot title.\u003C/p>\u003Cp>Oh, and that's not building because I did not close that off. Right. But now I'm not seeing anything. If I go to announcements, hey, I've got a card here. Nothing is rendering.\u003C/p>\u003Cp>And that's because if I take a look at my data, right, where are you, data. So inside this slug, I've got the space, we've got some posts here, but it's just returning an actual ID. How do I grab the actual post for this? So I go into fields. I could grab all the root level fields like this.\u003C/p>\u003Cp>And with the SDK, I can do the the TypeScript safe syntax is is actually specifying this out as an object. But for the sake of this, maybe I do, like, a double wildcard here. And now we could see, if I open this up, we can see the space, we get the post. It is fetching all of the content for that specific post. Right?\u003C/p>\u003Cp>Great. Looks like we've got a card. How do these cards look? Okay. So we got a header on the card.\u003C/p>\u003Cp>That's the the post image. Alright. So we got template. Be the card header. Then we just have a regular slot.\u003C/p>\u003Cp>P. Oh, where are you? Mister p tag. And here, this will be the header. We've got Nuxt Image.\u003C/p>\u003Cp>That'll be the v if post cover underscore image. The source would be post dot cover underscore image. Let me just close that up, see what we get. Okay. Now we're getting somewhere.\u003C/p>\u003Cp>Great. This is actually gonna be a NuxLink. Right? Do we wrap the entire card? How are they doing it here?\u003C/p>\u003Cp>Looks like the the entire image, and then I can click over here. Right? So we wrap the image in a Nuxt link, Nuxt link. For our spaces, we're gonna do this will be the post. We'll add, and then we'll do something like this where we have index.\u003C/p>\u003Cp>I think this will give us the routing that we want. Alright. What else was I missing? The actual Nuxt link. Right?\u003C/p>\u003Cp>2 a slash, we got what the space well, actually, this is gonna be the slug. Alright. Well, we've got the slug up there. And then we have post dot slug. Post dot slug.\u003C/p>\u003Cp>Yeah. Cool. And there's our Nuxt link. I will just copy this here. Run that one more time.\u003C/p>\u003Cp>Run it back. And does that give us where we want to go? Except it doesn't, does it? Okay. Okay.\u003C/p>\u003Cp>Yeah. No. Now we're navigating where we wanna go. Why do we not have a slug? Do we not have a slug on the post?\u003C/p>\u003Cp>We don't. That's why it's not showing up. Alright. So let's quickly add a slug to these posts. And, you know, we could even create a, an automation inside Directus that would do this for us.\u003C/p>\u003Cp>And I'll just quickly add a test log. Okay. So we go there. Test log is not showing up. Page not found.\u003C/p>\u003Cp>Why is that? Because we're not doing the actual right. So we got spaces slash spaces. K. Now we go back.\u003C/p>\u003Cp>We try it again. Right? Go back. Refresh, maybe. K.\u003C/p>\u003Cp>A spaces slash announcement post. Oh, the Alright. This needs to be an actual we need a folder for that. Right? What's the best way to set this up?\u003C/p>\u003Cp>Okay. So spaces actually becomes this. Right? This is the space, and then this is the index route for that space. This becomes anywhere that we're referencing slug becomes the actual space.\u003C/p>\u003Cp>Brand space space. Oh, let's not destructure that space. Slug. Does that work? Spaces, slug, Space.\u003C/p>\u003Cp>Space. Space. Space. What are we what are we missing? What are we missing?\u003C/p>\u003Cp>Let's just go back. Alright. Slug. Okay. So now I've got the space.\u003C/p>\u003Cp>Right? Let's just log the params out here. Params. Why can't I like any ears? Params, slug, spaces announcements.\u003C/p>\u003Cp>Oh, it has to do with the URL structure now. Right? We've changed that. So we go back to our tag here. We don't really need this.\u003C/p>\u003Cp>Okay. So at least now we're getting an error. And what else do we have here? Index dot view slug space slug. Give it to me.\u003C/p>\u003Cp>Input string space dot cover image. Oh. What am I doing wrong here? So there's our announcements. Okay.\u003C/p>\u003Cp>It's because we don't have any. This would be v if space dot post dot length greater than 0. Solve the the issues. It doesn't like that. Vcard.\u003C/p>\u003Cp>Vfpost.coverimage.source.post.coverimagespace. Why does it unlike the unlike the discussion page? No worries, though. So we change the routing there. Alright.\u003C/p>\u003Cp>So now we are now we're getting somewhere. Right? We've got the routing the way that they do it. And then inside this post, we will actually fetch the individual post as well. Alright.\u003C/p>\u003Cp>So we'll come here. Let's just call this post so we can render that out. The data will be post. We are going to fetch the post dot dash slug. And instead of spaces, we're gonna do post.\u003C/p>\u003Cp>And in this case, this one is actually called post. Alright. What else do we need? It's gonna grab our comments as well. Alright.\u003C/p>\u003Cp>Boom. Okay. So now we can see our post. Looking great. If we go back, we'll grab the header image.\u003C/p>\u003Cp>K. Post dot cover image. Do we have a cover image for that post? Yes. We do.\u003C/p>\u003Cp>Why are we not sorting through that post dot cover image? Input must be a string. Oh, because we're fetching all of the data. Let's just delete that. That's probably why.\u003C/p>\u003Cp>Okay. So there's our cover image. Let's do a div. We're gonna do VHtml. This is gonna render out our actual content.\u003C/p>\u003Cp>So post dot contents. And Telwin has some really nice typography styles already preset for you, and we are going to just use those. Right? Pros. Missing our intag.\u003C/p>\u003Cp>Alright. Let's wrap this again. Div. We've got p x 4. Give us some padding.\u003C/p>\u003Cp>K. There's our post. Let's get into the comments. Right? How do we get down to the comments?\u003C/p>\u003Cp>We'll do just give this a little breathing room. P x a p y 8. 6 is not nearly enough padding. I definitely need to go in and, update the the actual mobile views for this as well. And then we have our comments section.\u003C/p>\u003Cp>How we doing on time? 55 seconds. Man, this one has been a little bit of a struggle. A little more to it than what I thought originally. Also operating on not enough coffee.\u003C/p>\u003Cp>So it's it's pretty painfully obvious at this point. Not going to get to any of the comments. So if we look at our functionality, right, we managed to get here. Users can log in. Did we even get there?\u003C/p>\u003Cp>We actually did, because it was already baked in, but we didn't we didn't really mess with that. Alright? There's a auth login page where I could actually log in to this thing. Users create posts and add comments to those posts, create and manage events. Man, like, we did not do so great on this one.\u003C/p>\u003Cp>So, sometimes you fail. Circle is a really cool app. You know, recreating that functionality, there's a lot baked into it. So, you know, hey. What do we give this one?\u003C/p>\u003Cp>Like, Cisco and Evert would probably give this one 2 thumbs down, but that's the way it rolls. You know, we've learned a lot about the different data models and how to use the different relationships inside Directus. So let's chalk it up as a a tie, maybe. I don't know. That's it for this episode of 100 apps, 100 hours.\u003C/p>\u003Cp>Thanks for joining me. We'll see you on the next one.\u003C/p>","Hi guys. Welcome back to the next episode of 100 Apps, 100 Hours. I'm your host, Brian Gillespie, developer advocate at Directus. And today, we will be building a community platform. Now, this is one that is near and dear to my heart because I have built a community for sign and print shop owners myself. We use Facebook Groups for that. Not the best community platform, but it is free and everybody already has a Facebook account. But in just a quick look at the research for this episode, There's quite a few different tools out there that I located, Circle being one of the more popular ones. It's not exactly a forum, it's not exactly a course, it's not exactly chat, it's all of those things. So the pieces that we're really going to dive into today are this concept that they have of spaces. So each one of these items in the left navigation is a space. A space can be, either private or public. So I can see here some of the circle experts and this is their their own example community. Right? They do have courses in here. We've already done an episode on LMS, so we won't really touch on that. But, if we get into this, we could see what's new in the product. So this is an update we're gonna post. We've got some comments down at the bottom, I'm assuming, and you had to be logged in to comment. So how do we kinda recreate some of this functionality? It looks like they have weekly events here, but we're probably not seeing those. And maybe here's something similar. So anyway, that's what we're building. If you, have watched any of the other episodes, you probably already know the rules. We've got 60 minutes to plan and build, no more, no less. And the second rule, which is not really a rule, is use whatever you have at your disposal. So, any of the tools that are out there, AI, Tailwind CSS is one of my favorite ones. We're going to be building this platform on Directus as well. That'll be our back end. You could probably even actually build the entire thing inside Directus, but we'll probably go for a custom UI on the front end. Alright. So, sounds great. Let's rock and roll. So we'll start the 60 minute timer and away we go. So anytime I build an app, I always love to just kinda flesh out the functionality ahead of time. So if we're looking at this specific app, maybe I'll just pull up a text box inside Figma here. Here's the functionality that we really want out of this app. Right? We want to create and manage different spaces. Spaces for a how do I can I constrain this? Yeah. There we go. For a community. Maybe we have events as well that we're going to list. List and create and manage events. Events. We need to be able to log in. Users can log in. Create posts. Right? Is that what we're going to call that? Maybe a thread? We'll get into it. Right? We'll create some posts and add comments to those posts. That feels like a a pretty good chunk of functionality. I don't know if we'll get to all that or not. That's, like, actually building the UI for that, could be fun. Right? Alright. So let's just, like, flesh out our our actual data model here as well. So again, I'm not the savviest Figma user, but I try. I'm a tried. Alright. So what do we have? We've got spaces. So those are gonna be our little homes for all this different stuff, this different type of content. Right? Then we've probably got, this is where we get into the naming. Right? Is it a a thread? Is this a what what are these things called? Are these posts? Yeah. Let's go with that. Right? This is a post. And a post has some comments. Probably has a probably has, like, a post type, I'm assuming. K. Let's look at what else we've got here. Share your wins. Log in. Yeah. Okay. So these are different posts. You can upload images to a post. You can add comments. You can like. So we probably got some likes in there. And I don't know that we'll get to that either. But, what else do we have? We have users. That's gonna be taken care of by Directus because it gives us that out of the box, which is nice. That'll be our Directus users collection. And what else do we need for a function community? Right. Are events gonna be a separate thing than posts? I don't know. Let's we'll try it out. Cool. Let's just add that to the end here in case we we don't have time to get to it. Alright. So this feels pretty good. We're closing in on 4 minutes or so in the planning stage. Alright. So what do we have set up? We have, we got Directus going here. I've got a Directus instance that is totally blank, so I just reload. You'll see there are 0 collections. And on the front end, I've got a starter Nuxt application that just basically has the Directus module or the Directus plug in already preconfigured. We're using the SDK to communicate with the Directus instance, but I've just got Directus spun up on a local Docker instance. So, really fancy app here. We've got Tailwind. We're using Nuxt UI as well on that front end. So got quite a few things going on there. Alright. So first and foremost, we've got spaces. Let's go ahead and create our data model. Directus makes this super easy. We'll just go into our blank instance. We'll create our first collection. We're going to call it spaces. So those are the different spaces here. We'll generate the UID. We'll probably have a a status for the space. What's the sort order for the spaces? Who created these? When were they created? That's all pretty standard. Let's just take a look at the network requests on this to see if we can actually pick up some of their data model. Alright. Space groups. Alright. There's the circle community. Allow members to spaces. What else do we have? There's a slug for the space. So that's, that's a handy piece of information to have. Spaces, discussion. Okay. So we've got a cover image. Cover image URL. And this is via their API, so it doesn't necessarily represent the underlying database structure, but this data is coming from somewhere. So we're gonna have a name for the space. Name for the space. That's great. Alright. Do they have a description? Space description. Space name. We're definitely gonna have a slug, so that's great. We'll roll with that. Advanced field creation mode and, when I need something like a slug or a value that URL safe, if I go into advanced mode, I have Slugify available. So that will automatically transform the input as I type. Lock screen heading. I don't know, man. Let's just give it a description. And let's keep that non WYSIWYG. And last but not least, for our space, spaces, Let's add a cover image. So we've got our cover image. Right? And let's see if they had do they have a type of space? Space, slug. Visibility, post type is basic. I I wanna say there's probably some setting in here to control what these things look like, whether this is like a discussion or this is an event or a course. As we're fetching these, okay, that's a course. Internal spaces API. Maybe a type. Is there a type? Course type. Type is a banner. K. Those are yeah. No. I I don't wanna waste time on it. Right? Cool. So status, we'll just leave blank for now. Is this published or is it available? And then let's say let's add 1, like, is private. This will be a toggle. So the Boolean values Boolean value, I always used to struggle with that. That's what we'll save in the database. Is this space private or not? And then we can do a little cleanup here on our fields. So name, slug, description, maybe we pop that right beside there. And status wise, let's just roll with it. Right? So we go in, we create our first space. Maybe we wanna call this discussion, and that'll be at the slug for that will be discussion. Here's where we discuss stuff. Great. Is it private? Yes. Let's make it private. And then we will let's upload a cover image. Right? Unsplash. Let's just search for discussion. Okay. Images that are not seem to be working. Okay. Maybe some type of Internet issue. We're working here locally, anyway. Let's just grab from Circle. Right? We're already working with this specific thing. Do they have discussion? Where's the best practices for engagement? Yeah. Fine. Let's roll with it. Cool. Alright. So now we've got our first space. What else do we need to add to this? Right? So we've got posts for the spaces. Then each one of those posts could have comments, potentially. Right? So, let's just go in and create another. We'll just call it announcements as the next space. Announcements, read only channel for important announcements. Very fancy. Important announcements. Alright. So that is not private. Let's make both of these published just to to have it. Great. Okay, so we'll go back into our settings and let's add the next item. Let's add posts. We use the UUID. Do we have a status for the post? Yeah. Probably. Date created, date updated, is there a sort for the post? Do we wanna control the sort order on those? Yeah. Maybe potentially you do. Alright. So if we take a look at circle here. Let's dissect one of these posts. Right? So a post has a title, looks like. Also has a user that created the post. So Directus has taken care of that for us out of the box. We probably got a date that this was created. We've got some rich text for the post. So that'll be the post content, maybe? Post body? You do that. Let's just keep it as content. It seems pretty straightforward. Naming stuff is always the the hardest part about development. What else do we have? We got that cover image for the post. Right? Cover image for our post so that, you know, if we want to upload a cover image, we can. Sweet. Looks great. Let's go in and just create one of those posts. Right? So now we've got our post. Let's add a published post. And, Kate, if you're watching this, I'm sorry. I'm just gonna steal this post entirely. Use it verbatim inside this platform. And I'm also gonna steal your thumbnail image. Don't sue me, please. Alright. So we've got our post. Great. Looks savvy. Okay. Now we need to tie these together. Right? We don't have it's just floating out in space. It's not inside a space, it's in outer space. Alright, so let's tie that together. We're going to use the relationship indirectus for that. We'll just go to our spaces and one space could have many posts. But I think a post always belongs to a single space. We wouldn't want posts in multiple spaces. So that is a good example of our one to many relationship. There's one space, many posts, and these are gonna be called posts. The foreign key inside our post table, we'll just call that space without the s, because it's just gonna be 1. Alright. So we'll save this. And I can go into advanced mode inside Directus just to to make sure everything that I want is getting created. And maybe we I don't know if we actually need that sort field or not, but it's fine. Let's do a post title. I probably could have went with the post name, because we had a space name and not a title. But, again, we're splitting hairs here. There's no right or wrong when when you're naming things. There's only shades of gray. Some things are bad, some things are not. Alright. So we've got our spaces, we've got our posts, we've got a relationship between them. Right? So if I go into my spaces now and I click add existing, I can add that existing post to our announcements space. I could save that. And then if I were to go to our discussion, I could potentially pull that in here, but because of that relationship, it would decouple those. Alright. So next we've got, what, comments? Posts could have multiple comments. And let's do date created. We really don't have a status on these, do we? Like, you know, if the post if the comment is edited, we'll we'll be able to tell by the user updated field or the date updated field. So there we go. We've got the comments content, content body, comment message. Yeah. Let's roll with message. That's fine. So now we've got our comments. Let's send that relationship to the post. So we are, again, going to do a another one to many relationship. Right? Because we have a post. One post has many con comments. Okay. So oh, actually, we're on comments here. Right? So we need the inverse relationship of that. We've got the many to 1, because we were on the comments. So the key here will be post. Our related collection is posts. I'm gonna open up advanced because I am going to add that corresponding field inside the posts collection. That's gonna be comments. Great. We can add our title there. Cool. Okay. So we've got comment, we've got a post. Now if we were to go, actually, let's just hop over to our post, make sure this is showing. Alright. Maybe we wanna show the space that this is attached to as well. Go into my post. Here's the post. There's the title. Here's the space. I can go in and add a comment. Really simple. New comment. Bada bing bada boom. Got a comment for that space. I can see it here, though we can't actually read it. It's just showing the UUID. So I can go back in and maybe we clean this up a little bit where we got our spaces, got our posts. And here in the display template for this, I could show the actual message. And if I wanted to, I could even get into the the weeds a bit here and show the user avatar. Is it gonna be the avatar or maybe the actual avatar thumbnail? Yeah. There we go. Looking nice. Cool. Let's see what that looks like. Inside our post, new comment. We don't have an avatar for for little old me, so let's solve that really quickly as well. Just do right. Right ahead. Where are you? Me. There we go. Cool. We'll go ahead. Alright. So I could see myself that I made a comment. We can see the space it's attached to. What what are we missing from our functionality? Right? Comments, likes, and we could do that. Post, comments, likes. I you know, do we even really wanna do likes on this? Yeah. I think this is fine. Right? Let's let's roll with this for now and get something happening on our front end. Let's build something cool. I'm sure I'm missing something. But oh, what if we had, like, threaded comets? Right? I don't know if the circle support that type of maneuver. Looks like it does. Right? So, I can also have recursive relationships. So this will be a good look at what's available inside Directus as well. So I could go in and create recursive relationships. Boom. Boom. Boom. Boom. Boom. And, you know, we we could take care of this on the front end. Or, you know, if I wanted to show that recursion, we could do it here. So we'll add a parent key, and then we'll just reference that same collection. And let's do, like, children. Sounds cool. Great. K. And as far as the interface, I could also do, like, the tree view, which would show, like, nested recursive items, as it says. But, maybe we don't drill down any more than one layer here. Cool. Alright. So now I can have a comment. I can have, a parent underneath that. Parent comment. Oh, did I get this backwards? Yeah. Maybe I goofed that up a little bit. Name these the wrong things. So children. Yeah. I did, didn't I? Let's just sort that out. Right? The should have been a mini to 1. Okay. So that's the parents, and that's this comments collection. And then we have our children. Didn't necessarily have to add that one, but cool. Okay. Now now we should be looking good. Right? So I can add all the children. This is a child comment. Cool. Alright. So we're setting up a lot of this. This is gonna look nice on the front end. Before we get into the front end, I'm also gonna go in and add a new role to the back end here. So I'll just go in, we'll call this a user. They probably don't need app access. Right? We're going to build our own front end for this. And then we will give them access to see all the comments, see all the posts, see all the spaces. You know, do we want them to be able to create comments? Yes. We'll let them create posts. Can they create spaces? No. Can they edit comments? Yes. Potentially. Can they edit posts? Yes. They could edit posts. But let's do a custom permission where user created has to equal their own ID. So user created equals current user dot ID, and I'm not sure if this is actually user created dot ID. Let's try it that way just to be sure. Alright. So now they can only edit their own posts. We could do the same for comments. And, you know, do we want to let them delete their posts? Yeah. Probably do. But cool. Alright. So let's check the timer. We're looking good. And now let's start building something. Right? This is not much of an app to look at at this point, but maybe we drag this over and start building. Cool. Alright. So on the index page, let's just take a look and and dissect what we've got here. We've got like a nav on the left, then we've got the actual space here on the right. And I think that is it's pretty static. Right? So let's use our Nuxt layouts for this. We've got a default layout. We could see that in action here. Do we wanna use layouts, or do we wanna actually do a page for this? Let's do a page. So I could use, like, a Nuxt child route for this. What do you call this? What do they call their URLs? Right? We load this thing up. It's just slash home. Right? Slash c. Okay. Oh, looks like they've got slash c. Okay. That's part of all of these. Right? So we we could do something similar where we have, pages a dot view vcomp ts. Yeah. I'll I'll usually end up doing something like app dot view. Okay. So we're gonna have 2 sections here. We've got, like, a left sidebar, and then we have the content on the right hand side. So we can, like, flex this. And on the left sidebar, They make the is the width dynamic? No. You can't change the width on theirs. Probably a good thing. Easier that way. So let's just say with 56, we'll do p g gray 50. And then we have the, what, main content. Right? Sounds great. Div. And here, we're gonna put this Nuxt page component. Right? Now let's see what we got. Right? If we go to pages slash a, What did I do? Why is this not working? Pages slash a. Come on. Refresh. Next page. Next page. Oh. Slasha. Okay. Slash local host/a/app. No. Slasha. Okay. Yeah. Cool. So we got nothing here. Right? Nothing here. There it is. There we go. Okay. So now, let's pick up our spaces. We're gonna render these over here. If I look at the Nuxt UI library, one of the things that I like here is they have this vertical navigation component. So we're just gonna use that. Right? Here's the structure for it. We can pull that in. Let's just drop it at the end of this. Copy these links just so we get our structure. And then we'll put vertical nav in the sidebar. But, obviously, you gotta delete these other ones. Okay. So we now we could see something going on. Maybe this needs to be height full. What is our default look here? High screen, maybe. Okay. Alright. So now we're getting that running the full gamut. We'll probably build in a little bit of padding here. P y 8. K. Maybe we're gonna add a logo at the top at some point. But now how do we actually get our data from Brectus into this particular instance, into this app. Alright. We wanna pick up these spaces dynamically. What we're gonna do in that case is call the Directus API. Alright. So we're gonna do we could use the use async data composable, especially if this is gonna be accessible on the front end, without authentication or anything. Maybe you want a server side render this if it is a space that has, you know, public data on it. Alright. So we're gonna use async data. We give this a key. Let's just call this, root. And I think this is the syntax. Alright. So I've got a composable setup in my Directus module. Where are you, mister Directus module? There we go. Use Directus, and here we will do this. We're going to call use Directus, and I'm gonna use the SDK that we have here. So I'm gonna read the items from our spaces collection. And, you know, I could add in, like, a some query params here or, some parameters to filter that out, but let's just take a look at our data. So I'm already seeing in the console, we've got a server 403 fetch error forbidden. Right? That is because we are not logged in. So how do we solve that? Right? Inside our Directus module that I've got set up, I've got a an auth composable that will log us in and fetch the user and all of that, and then store that as the user and inside use state. And I've even got in a middleware. Right? So we could add a middleware to this page, where if we're not logged in, it is going to send us to the authentication page. So, you know, if this was actually public data. Right? Maybe we needed to adjust our permission settings. Right? So in public, I could go in, maybe people can read all the comments. They could see the spaces. They'll probably still be able to read the see the spaces. Right? It will probably just be like the post that you would not be able to see unless they are authenticated. Alright. So here's one way we can set that up. Right? I can go into my item permissions and go to my space. And if this is private, if it's private, I'm going to restrict. So is private equals nothing. So if if private is null, they could see all those posts. But now if I refresh, I should remove that error. And then if I look at my app, We could drill in a couple layers here. Where's my page? Alright. So we can see the spaces that we're getting back here. Great. Okay. So we got our individual spaces. Now let's ditch these links. Alright. And just map these out. So our links are gonna be, let's say, the let's call these spaces. And we'll just do something like this where nav spaces equals computed. So we'll just grab this data. And we are going to map this out. Right? So we'll do spaces dot value dot map, and we got a space. And for each space, we are going to return what, label. So that's gonna be the space dot name. Do we have an icon for these? I we didn't have an icon, but we could potentially set one up. Or maybe we don't even have an icon. We'll just omit that. And then we have 2. Right? So 2 is gonna be slash, and let's use a template literal here. We'll do slash dot space spaces/space.slug. Cool. Alright. And now if I do something like this, let's just log this out to make sure we're getting that. Spaces. Nav. Why am I not getting that? Next page. Where's our app? Slash a. Where are you, baby? What are you doing? There it is. There's our component. Now spaces cannot read property of undefined. If spaces if no spaces maybe we do this. Data equals unref our spaces. No. We'll just do this. If spaces, no spaces, we'll return empty array. So let's fix our issues. What do we get here? So spaces is still undefined. What am I doing wrong? Rich, doing something wrong up here in the actual call. Let's take a look at the use async data. Oh, duh, gosh. I need more coffee. So await async data. Why is this not still need do I need to return this? There we go. Fingers crossed. Fingers crossed. Refresh. Okay. So we got our spaces. We're still getting undefined on our computer here. Unrefspaces.map. Alright. Why is nav spaces undefined? Space dot name. So if we look at our spaces here, we have a name. We have a slug for those. Spaces is an array. We can confirm that. Do we have it now? Nope. Okay. That's always the fun of doing this. Oh, nav spaces. Undefined. What am I doing wrong here? Good question. Let's look at the clock. 23 minutes. Alright. So it looks like I just forgot to return this, basically. I think that solves the problem. Okay. Yeah. So now I can actually see that. Right? Kind of a bonehead move, but there we go. Sometimes it happens. So now, we've got our spaces over here and we could even, let's have a look at this component. I don't know if it will actually return. You can use the default slot or an avatar slot, customize the avatar, or give it an icon. Yeah. What if we just give it a, like, a circle icon? Icon is circle. Let's use the material symbols, is there material symbols here? Maybe circle. Sorry, icon. We'll just search all these. Right? They're just like a nice circle icon we could use. Yeah. It's fine. This is the material circle icon. Let's try that for all of these different items. Icon. Does that work? No. We need to change the syntax with Nuxt UI. Yeah. It's not showing up. Alright. So we'll just ditch the icon for now. Cool. Alright. Let's get on with building this thing. Right? We've got our main page, now we've got our spaces. If I click on the space, I don't have anything showing up for the space. So we'll go in and create a new folder and then we're going to do a spaces ID dot view. Right? The other thing that I'm going to do here is create a new folder called a and drop spaces in there as well. And then I need to go back here and adjust this to where it says a or app or whatever it is. We're just kind of following their convention here. Alright. So we've got spaces. Id. We'll just create a View component and spaces. Alright. So we click here. Are we on the space? Yep. And we could prove that just by doing something like this where we do cons route equals use route and route. Paramsiddiscussion. Right. And we could probably call this slug. It might be a better way just to keep everything clean, tight. And then we refresh, we can see slug is discussion. Okay. So how do we actually use this? Again, we're just gonna go in and fetch the space and we'll use async data, and we'll do spaces. And, actually, do we need the we're we're getting the space slug. Yeah. So we'll probably fetch that. Let's call it spaces dash. If you don't provide a, like, key for async data, it will use the the actual print. It will use the the spot that it's at in the file, like, the the file and the line number and things like that. But we'll just give it an actual key here to use when it caches things. Alright. So as far as our data, we're gonna return, we're gonna use Directus, we're gonna read the items from spaces and we are going to filter this down. Right? So the filter is at the slug underscore equals route dot params dot slug, you know, I could filter this out a little bit, Prayms route. I don't even know if this will work. Equal slug. Returned, use directus, slug. Unexpected keyword return. Do I not need the return? Oh, wait. Use async data. Oh, I forgot my function there. Okay. So we're gonna return, use directus, and, oh, I got a period on my slug. Cool. Are we actually getting data there? Let's take a look at that component. Right? This is the slug, so we'll just use our view dev tools. Do we have our space? Yes. We do. And then inside this use async data call, let's back this out. Right? Where are you? I can do is it transform? No. They have a a set of options. Transform, data, data 0. Let's see what that does. Now that should grab my single space. So the spaces are space because when I read items, I'm always returning an array. But using this transform on the async data call, I can transform that data and just pick up the first item from that array. So there's my space. I don't see any posts for discussion, but if we go to announcements alright. Do we see any posts there? Yeah. That's great. Okay. So, let's take a look at what we've got here for circle. We've got our discussion or our spaces over here, and then we have the discussion here. Alright. Cool. So we will add a header. It's got a cover image on it, basically. Let's try to find one where we did have some discussion. Right? Courses, engagement, Contest guidelines. Okay. Alright. So then we click into a post, and that is the assuming that's a space. Events new. I click into that. Events new. That's the slug for the post, I assume. Alright. So let's render out the header image for the space. Source is gonna be space dot cover underscore image. We'll make this full width. Maybe a height of 32. Object cover. See what we've got here. Just flip back. Input must be a string. Okay. So if we don't have the space dot cover image, space dot cover image, let's not try to render it. Is that the problem? Discussion. Okay. So I'm trying to get the image here, but what's happening? Right? Why are we not getting the image? It is because we are getting a 403 forbidden. We'll go into Directus here, go back to our access control. We have not allowed files to be shared. Okay? So there's our actual item. That's great. This is gonna be flex Flex 1. With full. Yeah. Okay. Alright. So we got our header. Let's just keep moving. Then we have our post detail. Right? I've got a v text component here that this will render, like, paragraph text. But, if we're gonna do something like the rich text for the post, So let's look at posts. We've got, like, this post content here. So we're great for a title. Alright. So let's just do, like, a h 2 or maybe an h one for the post. Not sure. Let's just do h two. Oh, we're not even showing the actual post. Right? Let's do an h one because we wanna show the space name. Alright. So there's the space name. It gives us a little padding. Pxpy6. Okay. Make this bold. Make it a little larger. 2 XL. Alright. So there we have that. Let's dive into the actual posts. Right? So 2 ways I could go about this. I could get all of the posts in a single call with Directus, or I could fetch these separately as well. In this case, let's just render some of these posts out. So we will go into our query here, and we'll do fields. Alright. So if I wanted to do, like, some cards so let's just back up and take a look at circle here. Right? We've got a couple of cards here that are showing up. Let's do the ucard, which is inside that Nuxt UI library. We're gonna do a post in space dot post. And Get a key. Key is gonna be post dot ID. And And then let's just render out post dot title. Oh, and that's not building because I did not close that off. Right. But now I'm not seeing anything. If I go to announcements, hey, I've got a card here. Nothing is rendering. And that's because if I take a look at my data, right, where are you, data. So inside this slug, I've got the space, we've got some posts here, but it's just returning an actual ID. How do I grab the actual post for this? So I go into fields. I could grab all the root level fields like this. And with the SDK, I can do the the TypeScript safe syntax is is actually specifying this out as an object. But for the sake of this, maybe I do, like, a double wildcard here. And now we could see, if I open this up, we can see the space, we get the post. It is fetching all of the content for that specific post. Right? Great. Looks like we've got a card. How do these cards look? Okay. So we got a header on the card. That's the the post image. Alright. So we got template. Be the card header. Then we just have a regular slot. P. Oh, where are you? Mister p tag. And here, this will be the header. We've got Nuxt Image. That'll be the v if post cover underscore image. The source would be post dot cover underscore image. Let me just close that up, see what we get. Okay. Now we're getting somewhere. Great. This is actually gonna be a NuxLink. Right? Do we wrap the entire card? How are they doing it here? Looks like the the entire image, and then I can click over here. Right? So we wrap the image in a Nuxt link, Nuxt link. For our spaces, we're gonna do this will be the post. We'll add, and then we'll do something like this where we have index. I think this will give us the routing that we want. Alright. What else was I missing? The actual Nuxt link. Right? 2 a slash, we got what the space well, actually, this is gonna be the slug. Alright. Well, we've got the slug up there. And then we have post dot slug. Post dot slug. Yeah. Cool. And there's our Nuxt link. I will just copy this here. Run that one more time. Run it back. And does that give us where we want to go? Except it doesn't, does it? Okay. Okay. Yeah. No. Now we're navigating where we wanna go. Why do we not have a slug? Do we not have a slug on the post? We don't. That's why it's not showing up. Alright. So let's quickly add a slug to these posts. And, you know, we could even create a, an automation inside Directus that would do this for us. And I'll just quickly add a test log. Okay. So we go there. Test log is not showing up. Page not found. Why is that? Because we're not doing the actual right. So we got spaces slash spaces. K. Now we go back. We try it again. Right? Go back. Refresh, maybe. K. A spaces slash announcement post. Oh, the Alright. This needs to be an actual we need a folder for that. Right? What's the best way to set this up? Okay. So spaces actually becomes this. Right? This is the space, and then this is the index route for that space. This becomes anywhere that we're referencing slug becomes the actual space. Brand space space. Oh, let's not destructure that space. Slug. Does that work? Spaces, slug, Space. Space. Space. Space. What are we what are we missing? What are we missing? Let's just go back. Alright. Slug. Okay. So now I've got the space. Right? Let's just log the params out here. Params. Why can't I like any ears? Params, slug, spaces announcements. Oh, it has to do with the URL structure now. Right? We've changed that. So we go back to our tag here. We don't really need this. Okay. So at least now we're getting an error. And what else do we have here? Index dot view slug space slug. Give it to me. Input string space dot cover image. Oh. What am I doing wrong here? So there's our announcements. Okay. It's because we don't have any. This would be v if space dot post dot length greater than 0. Solve the the issues. It doesn't like that. Vcard. Vfpost.coverimage.source.post.coverimagespace. Why does it unlike the unlike the discussion page? No worries, though. So we change the routing there. Alright. So now we are now we're getting somewhere. Right? We've got the routing the way that they do it. And then inside this post, we will actually fetch the individual post as well. Alright. So we'll come here. Let's just call this post so we can render that out. The data will be post. We are going to fetch the post dot dash slug. And instead of spaces, we're gonna do post. And in this case, this one is actually called post. Alright. What else do we need? It's gonna grab our comments as well. Alright. Boom. Okay. So now we can see our post. Looking great. If we go back, we'll grab the header image. K. Post dot cover image. Do we have a cover image for that post? Yes. We do. Why are we not sorting through that post dot cover image? Input must be a string. Oh, because we're fetching all of the data. Let's just delete that. That's probably why. Okay. So there's our cover image. Let's do a div. We're gonna do VHtml. This is gonna render out our actual content. So post dot contents. And Telwin has some really nice typography styles already preset for you, and we are going to just use those. Right? Pros. Missing our intag. Alright. Let's wrap this again. Div. We've got p x 4. Give us some padding. K. There's our post. Let's get into the comments. Right? How do we get down to the comments? We'll do just give this a little breathing room. P x a p y 8. 6 is not nearly enough padding. I definitely need to go in and, update the the actual mobile views for this as well. And then we have our comments section. How we doing on time? 55 seconds. Man, this one has been a little bit of a struggle. A little more to it than what I thought originally. Also operating on not enough coffee. So it's it's pretty painfully obvious at this point. Not going to get to any of the comments. So if we look at our functionality, right, we managed to get here. Users can log in. Did we even get there? We actually did, because it was already baked in, but we didn't we didn't really mess with that. Alright? There's a auth login page where I could actually log in to this thing. Users create posts and add comments to those posts, create and manage events. Man, like, we did not do so great on this one. So, sometimes you fail. Circle is a really cool app. You know, recreating that functionality, there's a lot baked into it. So, you know, hey. What do we give this one? Like, Cisco and Evert would probably give this one 2 thumbs down, but that's the way it rolls. You know, we've learned a lot about the different data models and how to use the different relationships inside Directus. So let's chalk it up as a a tie, maybe. I don't know. That's it for this episode of 100 apps, 100 hours. Thanks for joining me. We'll see you on the next one.","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,159,160,161,162,163,122,164,165,166,167],"cb4e067f-9507-4e18-ab9a-435565f9e653","8434838a-8e4f-489a-8da2-fbf10de5de6a","c997b25e-400c-4350-bba4-f63853d844f7","1109be0d-8ab5-479b-a052-8ad30d9ffb1c","dcb952e0-7d13-43ea-9b1c-f2ca63efd07d","0aae287d-3916-4d91-9310-25828998e562","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",1773850447867]