Many organizations want some ability to use a service to handle some of their functionality and customize the interface users are shown. This includes things like the names they see displayed, data they want to be shown, or some images they want to see. Giving them the ability to add their own branding is one way to add value to your own products.
In this Redwood tutorial, we'll make an app that will change formats depending on what user is associated with the page.
Create the Redwood app
First thing we need to do is spin up a new app. In a terminal, run:
yarn create redwood-app branding
When this is done, you'll have a bunch of new files and folders in a branding
directory. The main folders we'll be working in are the api
and web
folders. We'll start with some work in the api
folder first.
Setting up the model
Building our app by making the models for the database schema works really well in Redwood. I usually like to start here because it's one way you can start thinking about your business logic from the beginning.
We'll be using a Postgres database. Here are the docs to get Postgres installed locally. Let's start by updating the .env
file with a connection string for your local instance. Uncomment the DATABASE_URL
line and update the value. It might look something like this.
DATABASE_URL=postgres://admin:password@localhost:5432/branding
Now we can go to api > db
and open the schema.prisam
file. This is where we'll add our models. One thing we need to do is update the provider
value at the top to postgresql
instead of sqlite
. Next, you can delete the existing example model and add these.
model User {
id Int @id @default(autoincrement())
name String
info Info[]
image Image[]
layout Layout[]
}
model Image {
id Int @id @default(autoincrement())
name String
url String
user User @relation(fields: [userId], references: [id])
userId Int
}
model Info {
id Int @id @default(autoincrement())
balance Float
lastLogin DateTime
endDate DateTime
user User @relation(fields: [userId], references: [id])
userId Int
}
model Layout {
id Int @id @default(autoincrement())
name String
dataLocation String
imageUrl String
user User @relation(fields: [userId], references: [id])
userId Int
}
Usually, when you have relationships between tables like we have here, it's a good idea to seed your database with some initial values. You'll see this pretty often with apps that have dropdown menus or pre-defined user roles.
We'll be adding our own seed data in the seed.js
file. You can open that and delete all of the commented-out code in the main
function and replace it with this.
await db.user.create({
data: { name: 'Nimothy' },
})
await db.image.create({
data: {
name: 'Nimothy Profile',
url: 'https://res.cloudinary.com/milecia/image/upload/v1606580774/fish-vegetables.jpg',
userId: 1,
},
})
await db.info.create({
data: {
balance: 7.89,
lastLogin: new Date(),
endDate: new Date(),
userId: 1,
},
})
await db.layout.create({
data: {
name: 'MidLeft',
dataLocation: 'mid-left',
imageUrl:
'https://res.cloudinary.com/milecia/image/upload/v1606580774/fish-vegetables.jpg',
userId: 1,
},
})
Run migration
With our models and seed data in place, we can migrate the database with this command:
yarn rw prisma migrate dev
That will add the tables and columns with the defined relationships to your Postgres instance. To seed the database, we'll need to run:
yarn rw prisma db seed
This will add the placeholder data we created in seed.js
so that the relationships between tables and columns are met and don't cause errors with our app.
Since we've run the migration and seeding, we can move on to the back-end and front-end.
Making the back-end and front-end
We're going to make the functionality to add new layouts and new users to the app for now so that we can show how things update for the user. We'll also be adding a special page to show how these updates would actually affect users.
For the sake of this project, we're going to assume that adding new users and layouts is admin functionality that users of the app won't be able to see. Later on, we'll add the user view that applies the custom branding.
Adding the ability to create and update users and layouts only takes a couple of commands in Redwood. Let's start by making the users functionality with this:
yarn rw g scaffold user
This will generate the back-end GraphQL types and resolvers as well as adding new components to the front-end. We'll run this command one more time for the layouts functionality:
yarn rw g scaffold layout
You can take a look at the code Redwood generated to make all of this work on the front-end by going through the web > src
directory. There are new files under components
, layouts
, and pages
, plus Routes.js
has been updated. All of the new files you see were created by that scaffold
command for those two models.
The back-end code that supports new user and layout creation and the edit and delete functionality can be found in the api > src
directory. You'll see new files under graphql
and services
that hold the GraphQL types and resolvers that make all of the CRUD work and persists the data.
Now we have the CRUD for the front-end and back-end for these two models. You can run the scaffold
command to create the CRUD for the other models, but we don't actually need it. What we do need are the types for those models. We can generate those with a couple of Redwood commands:
yarn rw g sdl info
yarn rw g sdl image
The sdl
generator makes all of the GraphQL types and a resolver for the specified model. If you check out api > src > graphql
, you'll see the new types that were generated for info and images. Then if you look in api > src > service
, you'll see that a resolver has been made to handle a query for us for both info and images.
The reason we're adding these types is so the user types reference these so we need them to be available, even if we aren't adding the front-end piece.
Running the updated app
If you run your app with yarn rw dev
and navigate to localhost:8910/users
, you'll see a table and buttons for different ways to interact with the data. You should see something similar to this:
Go ahead and add a new user by clicking the "New User" button. This will open the form like this:
Now you can add a new layout for this new user by going to localhost:8910/layouts
and clicking the "New Layout" button. It'll bring up this form:
Show the user their custom view
Now that we've got the core functionality together to create users and associate layouts with them, we can create the custom view that they will see. To do that, we'll use Redwood to generate a page that will load a specific user's layout. Make a new page with this command:
yarn rw g page option
This will add a new page to the web > src > pages
directory and it will update the Routes.js
file with a new /option
route. If you navigate to localhost:8910/option
, you'll see this:
We need to update this page to show the user's layout by pulling some data from the back-end.
Querying for the user layout
In the web > src > pages > OptionPage
directory, open the OptionPage.js
file and add the following import to get your GraphQL query ready.
import { useQuery } from '@redwoodjs/web'
Then at the bottom of the file, right above the export statement, add this code for the query.
const LAYOUT = gql`
query layout($id: Int!) {
layout(id: $id) {
id
name
dataLocation
imageUrl
userId
}
}
`
This will give us a specific layout based on the id we pass to the query. We'll be manually setting this id to mimic what we might get from a prop from a different component. We'll add the variable for the id in our query hook. This will be added inside of the OptionPage
component:
const { loading, data } = useQuery(LAYOUT, {
variables: { id: 1 },
})
if (loading) {
return <div>Loading...</div>
}
We're using the useQuery
hook to execute the query we made earlier and we're manually setting the id of the layout we want to use. Then we're checking the load status for the data and rendering an indicator that the page is loading the content so that the user doesn't see an error before the fetch finishes.
The last thing we'll do is update the elements to show in the layout format we currently have loaded.
Updating the page
To show the right layout, we're going to install the styled-components
package. That way we'll be able to pass props to update the layout based on the user viewing the page. So in the web
directory in your terminal, run:
yarn add styled-components
Now we're going to import that package in the OptionPage.js
file.
import styled from 'styled-components'
Then we need to add a new styled component to handle the location of the image based on that user layout. We'll add this right above the OptionPage
component.
const Img = styled.img`
display: block;
position: absolute;
top: ${(props) => (props.dataLocation === 'mid-left' ? '35%' : 'unset')};
right: ${(props) => (props.dataLocation === 'mid-left' ? 'unset' : '0')};
width: 360px;
`
We're doing a simple update of the image location with an absolute position setup. This will let the image move independently of the other elements on the page so that the user sees it in the place they've selected. We're passing in the dataLocation
value as a prop.
Cleaning things up
Just a few finishing touches and we'll have this layout working. First, we need to add the Img
to OptionPage
. We'll delete the existing Link
from the return statement and add this image instead.
<Img src={data.layout.imageUrl} dataLocation={data.layout.dataLocation} />
We'll also add a little line to show the name of the current layout. This will go below the description of the file location.
<p>{data.layout.name}</p>
That's it! We've finished up this app. Now if you run the app with yarn rw dev
, you should see something similar to this.
If you update the id
in the query variable to 2
and reload the browser, you'll see something like this.
Finished code
If you want to check out the complete code, you can check it out in the custom-app-branding
folder of this repo. You can also check out the front-end in this Code Sandbox.
Conclusion
If you're interested in a deeper dive on how Redwood handles scaffolding or the general way it creates files for you, make sure to go through their docs.