🕰️ Chapters
- Adding Avatar File Input Field
- Adding AvatarUrl Hidden Field
- Validating Avatar and AvatarUrl Fields
- MultipartFile Options
- Moving the Uploaded Image to Storage
- Storing the Image's Storage Path
- Removing A User's Avatar
- Enctype Multipart Form Data
- Downloading Stored Avatar Images
- Displaying Stored Avatar Images
- Allowing Users to Remove Their Avatar
- Deleting the User's Avatar Image on Remove
- Testing Our Upload & Remove Flow
- A Few Things To Notes
Uploading and Displaying User Avatars
In this lesson, we'll learn how to validate and upload avatar images into our project's storage. We'll then learn how we can easily display images we have contained within our app's storage
- Author
- Tom Gobich
- Published
- Jun 01
- Duration
- 15m 29s
![Tom Gobich](/img/1/profile/avatar_1703713552864.jpg)
Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.
Adocasts
Burlington, KY
Transcript
Uploading and Displaying User Avatars
-
(upbeat music)
-
So it's pretty common for applications to allow users
-
to upload a photo representation of themselves
-
into our application that we refer to as an avatar.
-
So today we're gonna cover how we can go about
-
adding a field for that into our user profile here.
-
So let's go ahead and hide our browser away
-
and jump into our profiles edit page.
-
Now with the way that we set up our form inputs,
-
we can specify a type and a valid input type is file.
-
So if we wanted to add in a file input,
-
we could do @form input, specify the type here as input,
-
give it a label of something like avatar
-
and a name of avatar.
-
And now on our actual user model here,
-
if we jump into here, scroll down a little bit,
-
we have the storage column for this
-
referred to as our avatar URL.
-
Rather than taking our avatar image as a base 64
-
and storing it directly inside of a database,
-
what we're gonna do is take that image, move it somewhere,
-
and then store a reference of where actually
-
we've moved it to inside of our database
-
as our avatar URL here.
-
So in terms of a default value,
-
if a user already has an uploaded avatar,
-
we're not gonna have an actual file
-
to be able to append in here as the value,
-
but we can still have that represented inside of our form
-
as a hidden input.
-
So we could do input type hidden name avatar URL value equals
-
and then we could set the default value
-
to our auth user.avatarURL.
-
That way, if they do have an avatar URL,
-
it will still allow us to discern
-
whether or not they wanna keep it
-
or remove it altogether as we send up data with this form.
-
And my bad, I've wrote input here as the type
-
that should be file.
-
Okay, so if we take a look at this real quick,
-
inside of our browser,
-
we should now see an avatar field
-
that when clicked allows our user
-
to jump into their file system
-
and select an image to upload.
-
Cool, let's hide that back away.
-
Let's next jump into our profile validator
-
and we're gonna add in two properties.
-
First, we're gonna add in our avatar.
-
This is the representation of the actual image
-
that we're uploading.
-
So we'll do vine dot and there is a file type here
-
and it accepts in a number of validation options.
-
Some of the more prominent ones
-
that you might want to validate against
-
would be extension names.
-
So these would be actual file extensions
-
that we want to allow users to upload.
-
So that might be something like JPEG, PNG, JPEG,
-
and any other file types applicable
-
that you would want to accept.
-
Essentially, this allows us to say
-
that we don't want users to be able to upload a PDF
-
as their avatar image.
-
And then we can also add in a size
-
to limit the file size that the user can upload with.
-
So if we wanted to limit our upload
-
to something like five megabytes,
-
we could do five MB there to discern that here
-
with our avatar upload.
-
And then of course,
-
we also want to make this field optional altogether.
-
We're not gonna require users to have an avatar.
-
It's gonna be completely optional.
-
And then we also have our avatar URL.
-
This is what we use to mostly discern
-
whether or not a user wants to remove their avatar.
-
Since our actual avatar field
-
is only ever gonna have a value
-
if they want to upload an avatar,
-
we can use the avatar URL to persist
-
whether or not they want to keep
-
a previously uploaded avatar image
-
as their current avatar.
-
Or if our avatar URL were to come up empty,
-
then we would know that they are intent
-
on removing their avatar image.
-
Okay, so file.
-
And this is just gonna be a string
-
because this is a string representation
-
of where we've stored their avatar.
-
And it too is gonna be optional.
-
Cool, so let's give that a save.
-
And then we'll jump into our profiles controller.
-
We now have from our validator an avatar
-
and an avatar URL that we can extract out.
-
Now, a thing to note before we get started
-
is our avatar comes through
-
since we have it as a file on our validator
-
as a multi-part file.
-
So this is gonna have additional information on it
-
like the file name that the user's uploaded the file with.
-
And it's gonna have options
-
that we can take with that file,
-
like being able to move it somewhere else.
-
When we upload a file via a form like this,
-
the body parser is gonna move this
-
into a temporary folder called TMP
-
inside of our application.
-
So that allows us to then move that file wherever we want.
-
That could be uploading it to a service like S3,
-
Google Cloud Storage, Delogean Spaces, and the like,
-
or somewhere inside of our application structure
-
that we want to persist those images.
-
The way we can do this is if we scroll down here,
-
let's do this just underneath where we're querying
-
our profile.
-
If we have an avatar that's come up,
-
we can await, reach for our avatar.
-
And here's a quick autocomplete list
-
of all of the properties and methods
-
that the multi-part file has that we can make use of.
-
So you can see it comes with a file size,
-
subtype, temporary path location,
-
whether or not it's been validated.
-
We scroll up a little bit further.
-
There's the file path, file name, field name,
-
extension names, whether it came up with any errors
-
and the like.
-
But what we care about right now is a method called move.
-
So if we call this, it's going to accept in
-
as its first argument, a location that we actually want
-
to move the avatar to.
-
Remember by default, this is going to upload
-
to a TMP directory in the root of our application.
-
So let's say instead we want to move it to a folder
-
inside of our application called storage,
-
and then a sub folder inside of storage
-
specifically for our avatars.
-
We can use the app module.
-
So if we type out app here and hit tab to auto import this
-
from AdonisJS core services app,
-
we can call a method called make path
-
that allows us to make and or get a reference
-
to a path inside of our application structure.
-
So if we wanted to move this avatar
-
into a folder called storage, we could do storage.
-
And if we wanted a sub folder called avatar,
-
we could do slash avatars.
-
And now if storage slash avatars already exists
-
inside of the root of our application,
-
then this will just return back a reference
-
to that location.
-
Otherwise it will create a folder called storage
-
and a sub folder called avatars if needed.
-
Additionally, with this move method,
-
we also have some other options.
-
So we can rename the file and specify whether or not
-
we want to allow it to overwrite a file
-
already inside that location.
-
We'll leave both of these off for now.
-
Cool, so this will take care of storing our avatar
-
where we want it inside of our application.
-
Now what we need to do is store that location
-
as our avatar URL path on our user.
-
So we can do auth user avatar URL equals.
-
And now this app dot make path is going to be an absolute
-
path in our file system to this location.
-
And that's not what we want to store as our avatar URL,
-
but rather we just care about the location
-
where we're gonna be able to access this going forward.
-
So we might save this as something like avatars slash,
-
and then the avatar file name.
-
And the file name is also going to include
-
the file extension, so we can leave that off.
-
And then that will get persisted with our usual merge,
-
save call right down here.
-
So we don't need to call save here too.
-
Lastly, if a user is not uploading an avatar,
-
then they may be trying to remove their avatar.
-
So we can do else if not avatar URL,
-
then we just want to clear that on our user.
-
So auth user dot avatar URL equals,
-
and we can just null that out.
-
Cool, so we should have most everything set up.
-
There is an additional thing that we want to do,
-
but I want to show how things behave
-
before we make that change.
-
So let's jump back into our browser
-
and let's attempt to upload an avatar here.
-
So let's select an image.
-
I have an image right here.
-
We'll just go ahead and open it up.
-
Okay, you can see it's selected right here as the file name.
-
If we go and just hit update avatar,
-
you'll see that we're gonna get back an invalid validation
-
for our file type, the avatar must be a file.
-
Now, the reason that DonetsJS isn't able to recognize
-
our avatar as an actual file
-
is because we haven't specified on our form
-
that we want to allow this to happen
-
via an anti-multi-part form data.
-
So if we had our browser back away,
-
go back into our edit page,
-
scroll up to where we have our form.
-
What we want to do is add an attribute called ENC type
-
and set this to the multi-part form data value.
-
This is gonna allow our form to upload in multiple parts
-
so that our server can actually digest
-
all the bits of the image
-
and actually recognize it as an image.
-
So with that applied, if we give this a save
-
and jump back into our browser
-
and give this a try once more.
-
So let's go ahead and select our avatar image here,
-
hit open and update our profile.
-
You'll see that we get redirected right back here
-
with no errors applied on our avatar field anymore.
-
Furthermore, if we hide this away real quick
-
and we add a quick inspect of our auth user avatar URL,
-
jump back into our browser,
-
you can see that we have now avatars/av.jpg,
-
exactly as we are.
-
So next, what we want to do is display that avatar
-
above our avatar field and allow the user to hit X on it
-
to clear it out,
-
which would then clear out our avatar URL field.
-
Before we do that though,
-
we need to give ourselves a way to access our storage path.
-
If we had stored the image inside of our public directory,
-
it would have been immediately accessible
-
just via slash whatever the file path is.
-
But we've stored this inside of storage
-
because public is prone to getting cleared out
-
and updated every time that we deploy out our application
-
inside of a production-based environment.
-
But since storage is not publicly accessible,
-
like our public directory is,
-
we need to jump into our routes file.
-
And I'm gonna put this up at the top of all of our routes.
-
So scroll up to where we have our home route,
-
we'll put it just underneath there.
-
So we'll do router.get.
-
And since we uploaded our image itself
-
inside of storage/avatars,
-
we'll assume the storage part
-
and just specify that we want to reach for avatar images
-
by doing /avatars here,
-
and then we can do /filename.
-
So that anytime that we attempt to reference
-
just the storage path that we've saved our avatar URL as,
-
it'll come up exactly like this.
-
We'll have avatars/filename.
-
Actually, let's also go ahead and prefix the uploaded image.
-
So let's jump back into our profiles controller here
-
and prefix the avatars URL here with a slash two.
-
Okay, and we also forgot to take a look
-
at what actually uploaded.
-
So if we scroll down here a little bit,
-
we're gonna see a storage directory
-
now inside of our application.
-
If we click on this, there's our avatars
-
and there's our av.jpg right there.
-
You also see this temp directory.
-
This is where this image was temporarily stored
-
as we were uploading it with our multi-part form data.
-
Whenever we called avatar.move,
-
that's where it moved it from that temp directory
-
into our storage avatars folder.
-
Now we'll need to retest our upload here.
-
So I'm gonna go ahead and right click on the image,
-
delete it out and move to trash.
-
This will just allow us to persist that extra slash on there
-
by resubmitting our form.
-
Okay, let's jump back into our routes now
-
and let's make a new controller for our avatars.
-
So hit command + shift + P, make controller,
-
and we'll call this avatar.
-
Hit enter there.
-
It's not a resource, so we'll hit no there.
-
And there we go.
-
And we'll go ahead and add a show method onto there.
-
We'll circle back to here momentarily.
-
Let's go ahead and finish defining our route.
-
Okay, so we have our avatars controller
-
and we have our show method inside of there
-
is avatars show.
-
Now let's jump back into our avatars controller
-
and we'll want the response
-
as well as our route parameters.
-
For this, all that we need to do is download the image
-
and return it back as the response.
-
So we can return response, download,
-
and then we can use our app module again.
-
Hit tab.import that, make path,
-
reach for storage/avatars.
-
And we can specify a comma here
-
and add in our params.filename
-
to add that into our make path.
-
Let's give that a save.
-
Jump back into our edit page.
-
And now we should be able to replace this inspect here
-
with an actual image.
-
So let's do div class relative
-
and then plot the image inside of there
-
with a source, auth, user, avatar URL.
-
We'll do class width full
-
since we have this form section here pretty small,
-
and that should suffice.
-
We'll also want to wrap this in an if.
-
So if auth user does have an avatar URL, just like so,
-
and our if, and let's go ahead and see what we get.
-
So let's jump back to our browser.
-
And all right, we deleted it.
-
So let's go back into browse,
-
hit our image, open, update, and there we go.
-
So now we see our image
-
because we've re-uploaded it and fixed the path.
-
I don't quite like seeing myself that big,
-
so I'm gonna go ahead and do width one third
-
and mix auto on that relative div there.
-
And now what we want to do is add in a button.
-
So we'll do button type, button class,
-
and then we'll absolutely position this
-
to the top zero, right zero,
-
add a padding of maybe two or three, rounded full,
-
and we'll just do and times there to add a little X.
-
So let's start by breaking our button down here
-
and let's add an on click attribute to it.
-
Whenever we click, what we want to do
-
is both clear out the value of our avatar URL
-
as well as remove the parent element to this button,
-
which would be the div containing our image preview.
-
The easiest way to get a direct reference
-
to our avatar URL field is through the form.
-
So what I'm gonna do is scroll on up to our form,
-
add an ID, we'll just call this something like edit form,
-
and then we can scroll back down to our button.
-
And this will allow us to do document.forms,
-
reach for our edit form,
-
which allows us to reach directly
-
for our avatar URL field and its value
-
so that we can set it back to an empty string.
-
Let's go ahead and wrap that in parentheses.
-
Then once we've cleared out our avatar value,
-
what we want to do is an or,
-
since this won't necessarily come back truthy,
-
and reach for this,
-
which is our actual button element, .parent element,
-
which is now our div surrounding our button, .remove
-
to actually remove the div
-
and all of its subcontents from the document.
-
Just so that we can see this all evidently happen,
-
let's go and switch our avatar URL
-
from type hidden to type text,
-
just so that we can see it directly inside of our document.
-
Jump back into our browser.
-
There we go.
-
I'm gonna give this a hard refresh
-
to make sure nothing's going on there.
-
Here's our little X that will allow us
-
to clear everything out.
-
If we go ahead and give that a click,
-
there goes our avatar URL value,
-
as well as our avatar preview.
-
And now if we go ahead and update our profile,
-
our avatar is gone,
-
and we should also probably remove the file
-
from our file system and set this
-
with a default value of an empty string.
-
So first let's set this to a default value of an empty string
-
if we don't have an avatar URL, just like so.
-
And go ahead and switch this back to a type of hidden
-
since we've seen it in action.
-
And let's jump back into our profiles controller
-
where we have our else if,
-
and we're clearing out our avatar URL.
-
If you take a look at our storage avatars,
-
we still have our image in here,
-
despite it no longer being bound to our auth user.
-
Well, what I wanna do is bring the FS module
-
back into our application,
-
so we can import a method called unlink
-
from FS and FS promises.
-
Give that a save real quick.
-
I wanted a node prefix, there we go.
-
Okay, if we scroll back down now,
-
we can do a wait, call unlink,
-
which accepts in a path,
-
which allows us to delete out our file as noted right here.
-
So what we wanna do is provide a path to that file,
-
and we'll wanna do this
-
before we remove our avatar URL value.
-
So we'll do app make path to get an absolute path
-
to where we're storing our avatars,
-
prefix this with storage,
-
and then provide in our auth user avatar URL
-
as the second portion to that.
-
Now it's not quite happy
-
because avatar URL here could be null.
-
So to our else if,
-
we'll also want to check and end auth user avatar URL
-
to verify that the user no longer wants an avatar URL,
-
but did have an avatar URL to start with,
-
fixing the nullable reference here on our unlink call.
-
Let's give that a save.
-
I'm gonna go ahead and clear out our AV
-
just so that we can walk through the flow one more time.
-
Delete that out, move it to the trash, there we go.
-
Since we've already destroyed the reference
-
to our avatar URL on our user,
-
let's go ahead and jump back into our browser,
-
browse for our file, let's give it a select,
-
open it up, update, there we go.
-
If we jump back into our text editor,
-
there it is once more.
-
If we go ahead and hit on our X,
-
update our profile once more.
-
Now if we jump back into our text editor,
-
there we go, our storage avatars
-
no longer contains our image because it was removed.
-
Awesome, so there are a couple of things
-
that you're gonna wanna keep in mind here
-
before we round things out.
-
First and foremost,
-
we do have our storage directly inside of our application.
-
So depending on how your deployment
-
for your application works,
-
you may need to literally move that storage
-
from the old location to the new location
-
every time that you deploy out,
-
which could be time consuming and resource intensive.
-
Instead, what you may wanna do is dot dot slash
-
on your storage, which would give you a fixed location
-
outside of your application route
-
that you could store your files.
-
Another thing that you're gonna wanna keep in mind
-
as you're working with files
-
and especially your directory system
-
is ensuring that you're not giving users access
-
to be able to search through your file system.
-
So here we have a route specifically to /avatars
-
with a file name route parameter
-
that specifically reaches into our storage avatars location.
-
This could potentially open you up to the possibility
-
of somebody reaching inside of your application
-
going to something like say localhost3333/avatars
-
to reach a valid avatar URL slash.
-
A user could attempt to do dot dot slash
-
to go back a directory,
-
which would then be our storage dot dot slash again
-
to go into the root of our application
-
and then reach for something that we have inside of there,
-
say something like readme.d.
-
With how we've written things in AdonisJS itself,
-
we're protected against this,
-
but it is something to keep in mind.
-
So like if we were to hit enter here,
-
those dot dot slashes and everything before it
-
are gonna get stripped out
-
and we're not gonna be able to find a route for this,
-
but just keep that possibility in mind,
-
be sure to check against it as well.
-
Introduction
-
Fundamentals
-
2.0Routes and How To Create Them5m 24s
-
2.1Rendering a View for a Route6m 30s
-
2.2Linking Between Routes7m 52s
-
2.3Loading A Movie Using Route Parameters9m 18s
-
2.4Validating Route Parameters6m 7s
-
2.5Vite and Our Assets6m 39s
-
2.6Setting Up Tailwind CSS7m 5s
-
2.7Reading and Supporting Markdown Content4m 32s
-
2.8Listing Movies from their Markdown Files8m 51s
-
2.9Extracting Reusable Code with Services7m 4s
-
2.10Cleaning Up Routes with Controllers4m 52s
-
2.11Defining A Structure for our Movie using Models9m 38s
-
2.12Singleton Services and the Idea of Caching6m 11s
-
2.13Environment Variables and their Validation4m 16s
-
2.14Improved Caching with Redis10m 44s
-
2.15Deleting Items and Flushing our Redis Cache6m 46s
-
2.16Quick Start Apps with Custom Starter Kits6m 28s
-
2.17Easy Imports with NodeJS Subpath Imports8m 40s
-
-
Building Views with EdgeJS
-
3.0EdgeJS Templating Basics8m 49s
-
3.1HTML Attribute and Class Utilities6m 9s
-
3.2Making A Reusable Movie Card Component10m 24s
-
3.3Component Tags, State, and Props4m 53s
-
3.4Use Slots To Make A Button Component6m 56s
-
3.5Extracting A Layout Component5m 13s
-
3.6State vs Share Data Flow2m 59s
-
3.7Share vs Global Data Flow6m 8s
-
3.8Form Basics and CSRF Protection6m 13s
-
3.9HTTP Method Spoofing HTML Forms3m 3s
-
3.10Easy SVG Icons with Edge Iconify7m 57s
-
-
Database and Lucid ORM Basics
-
4.0Configuring Lucid and our Database Connection4m 3s
-
4.1Understanding our Database Schema9m 35s
-
4.2Introducing and Defining Database Migrations18m 35s
-
4.3The Flow of Migrations8m 28s
-
4.4Introducing Lucid Models5m 43s
-
4.5Defining Our Models6m 49s
-
4.6The Basics of CRUD11m 56s
-
4.7Defining Required Data with Seeders11m 11s
-
4.8Stubbing Fake Data with Model Factories13m 48s
-
4.9Querying Our Movies with the Query Builder15m 30s
-
4.10Unmapped and Computed Model Properties3m 24s
-
4.11Altering Tables with Migrations7m 6s
-
4.12Adding A Profile Model, Migration, Factory, and Controller2m 57s
-
4.13SQL Parameters and Injection Protection9m 19s
-
4.14Reusable Query Statements with Model Query Scopes8m 11s
-
4.15Tapping into Model Factory States9m 15s
-
4.16Querying Recently Released and Coming Soon Movies4m 59s
-
4.17Generating A Unique Movie Slug With Model Hooks7m 59s
-
-
Lucid ORM Relationships
-
5.0Defining One to One Relationships Within Lucid Models5m 49s
-
5.1Model Factory Relationships2m 54s
-
5.2Querying Relationships and Eager Vs Lazy Loading5m 17s
-
5.3Cascading and Deleting Model Relationships5m 16s
-
5.4Defining One to Many Relationships with Lucid Models6m 56s
-
5.5Seeding Movies with One to Many Model Factory Relationships5m 24s
-
5.6Listing A Director's Movies with Relationship Existence Queries8m 41s
-
5.7Listing and Counting a Writer's Movies8m 41s
-
5.8Using Eager and Lazy Loading to Load A Movie's Writer and Director5m 18s
-
5.9Defining Many-To-Many Relationships and Pivot Columns9m 48s
-
5.10Many-To-Many Model Factory Relationships4m 50s
-
5.11A Deep Dive Into Relationship CRUD with Models18m 5s
-
5.12How To Create Factory Relationships from a Pool of Data13m 55s
-
5.13How To Query, Sort, and Filter by Pivot Table Data9m 47s
-
-
Working With Forms
-
6.0Accepting Form Data12m 15s
-
6.1Validating Form Data with VineJS9m 29s
-
6.2Displaying Validation Errors and Validating from our Request7m 16s
-
6.3Reusing Old Form Values After A Validation Error2m 3s
-
6.4Creating An EdgeJS Form Input Component5m 28s
-
6.5Creating A Login Form and Validator5m 1s
-
6.6How To Create A Custom VineJS Validation Rule9m 7s
-
-
Authentication & Middleware
-
7.0The Flow of Middleware7m 49s
-
7.1Authenticating A Newly Registered User4m 14s
-
7.2Checking For and Populating an Authenticated User2m 10s
-
7.3Logging Out An Authenticated User2m 24s
-
7.4Logging In An Existing User6m 54s
-
7.5Remembering A User's Authenticated Session6m 55s
-
7.6Protecting Routes with Auth, Guest, and Admin Middleware5m 36s
-
-
Filtering and Paginating Queries
-
8.0Creating A Movie List Page3m 43s
-
8.1Filtering A Query By Pattern Likeness7m 9s
-
8.2Filtering Our List by Movie Status5m 47s
-
8.3How To Apply A Dynamic Sort Filter To Your Query7m 12s
-
8.4Joining SQL Tables To Order By A Related Column4m 49s
-
8.5Validating Query String Filter Values7m 24s
-
8.6How To Paginate Filtered Query Results9m 15s
-
8.7Pagination First, Last, Next, and Previous Buttons4m 3s
-
-
User Watchlist
-
9.0An Alternative Approach to Many-To-Many Relationships4m 56s
-
9.1Toggling A Movie in an Authenticated User's Watchlist9m 56s
-
9.2Listing and Filtering User Watchlist Items7m 31s
-
9.3Allowing Users To Toggle A Movie As Watched4m 44s
-
9.4Filtering By User's Watched Status6m 7s
-
9.5Defining A Composite Unique Constraint4m 47s
-
9.6Persist Filters Easily with Lucid's Query String Method3m 58s
-
-
User Profiles
-
10.0How to Create and Fix Missing User Profiles in Your Application7m 37s
-
10.1Using Dependency Injection to Update A User's Profile9m 46s
-
10.2Saving All Or Nothing with Database Transactions5m 15s
-
10.3Uploading and Displaying User Avatars15m 29s
-
10.4Displaying A User's Profile6m 1s
-
10.5Filtering, Preloading, and Sorting By Relationship7m 6s
-
-
Admin Panel
-
11.0Creating An Admin Layout7m 14s
-
11.1Counting Stats for our Admin Dashboard5m 43s
-
11.2Paginated Admin Movie Table13m 2s
-
11.3Allowing Admins to Create Movies16m 39s
-
11.4Allowing Admins to Update Movies and Clear Values13m 27s
-
11.5How To Use One Form to Create or Edit Movies5m 32s
-
Uploading Movie Cover Images in our Create or Edit Form10m 29s
-
Using A Wildcard Route Param to Download Storage Images7m 57s
-
Posting Objects, Arrays, and an Array of Objects in HTML Forms26m 26s
-
Managed Transactions and Syncing Movie Cast Members15m 55s
-
Allowing Admins to Delete Movies and their Relationships7m 42s
-
Thank You for Watching!0m 31s
-
Join The Discussion! (0 Comments)
Please sign in or sign up for free to join in on the dicussion.
Be the first to Comment!