Section 10: Implementing Editable Projects
In this section, we'll cover how to create an editable Projects within our application. This process involves creating a new Project from a Template, enabling editing in our storefront, and handling the backend server configuration to support these features.
Objectives
- Transform templates into editable projects.
- Implement necessary backend endpoints.
- Integrate Studio egine editing functionality into our
editor.html
page.
1. Converting Templates to Projects
To initiate template editing, the user will press the Edit
button in the storefront, which triggers a request to our server. Here's the process to handle this action:
In server.js
, incorporate the following code to define an endpoint, called by store-front.html
that creates a new project based on a template:
app.post("/user/project/:templateid", async (context) => {
const userType = getUserType(context);
if (userType == "unauth") {
return context.text("Unauthorized", 401);
}
const {templateid} = context.req.param();
const dangerousToken = await getDangerousToken(db);
const project = await createNewProject({
templateID:templateid,
token: dangerousToken,
baseURL: baseURL,
environment:environment,
});
db.addProjectToUser({userID:"user", projectID: project.id, projectName: project.name});
return context.json({id:project.id});
});
This endpoint validates the user, creates a new project using the specified template ID, and then adds this project to the database.
2. Implementing the New Project Creation Function
To convert a template into a project, we define the createNewProject
function in chili.js
:
export async function createNewProject({ templateID, token, baseURL, environment }) {
const url = `${baseURL}/grafx/api/v1/environment/${environment}/projects`;
const headers = {
'Accept': '*/*',
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
};
const body = {
template: {
id: templateID
}
};
try {
const response = await fetch(url, {
method: 'POST',
headers: headers,
body: JSON.stringify(body),
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`Error ${response.status}: ${errorBody}`);
}
// Parse the JSON response
const responseData = await response.json();
// Only need the id and name from the response
const result = {
id: responseData.id,
name: responseData.name,
};
return result;
} catch (error) {
// Handle errors, such as network issues
console.error("Failed to create new project:", error);
throw error;
}
}
This function makes an API request to create a new project from the specified template and returns the resulting project's ID and name.
Don't forget to import our new function in server.js
.
4. Setting Up the Editor Page Endpoint
The editor page allows users to modify their project. In server.js
, add the endpoint for serving the editor page:
app.get("/editor/:projectid", async (context) => {
const userType = getUserType(context);
if (userType == "unauth") {
return context.redirect("/")
}
const storefrontPage = await fs.promises.readFile("public/editor.html", "utf8");
return context.html(storefrontPage);
});
This endpoint serves editor.html
when the project needs to be edited.
Updating editor.html
Below you will find the content (open the tab) to update editor.html
.
editor.html content
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Project Editor</title>
<script src="http://spicy-labs.com/external/allow/bundle.js" ></script>
</head>
<body>
<h1>Project Editor</h1>
<button onclick="saveProject()">Save Project</button>
<button onclick="orderProject()">Order Project</button>
<div id="studio-editor" style="height:70vh"></div>
<script>
// Extract the project ID from the URL
const pathArray = window.location.pathname.split('/');
const projectID = pathArray[pathArray.length - 1];
document.addEventListener("DOMContentLoaded", loadProject);
// Load Project Function
async function loadProject() {
const SDK = new StudioSDK({
editorId: "studio-editor"
});
SDK.loadEditor();
window.SDK = SDK;
try {
// Fetch the auth token first
const authResponse = await fetch('/user/authdata');
const authData = await authResponse.json();
if (authData.success && authData.token) {
// Store the token in a variable
const authToken = authData.token;
const environmentAPI = authData.apiURL;
window.SDK.configuration.setValue("GRAFX_AUTH_TOKEN", authToken);
window.SDK.configuration.setValue("ENVIRONMENT_API", environmentAPI);
// Now fetch the project data using the project ID
const projectResponse = await fetch(`/project/${projectID}`);
const projectData = await projectResponse.json();
// Load the document using SDK
window.SDK.document.load(projectData);
} else {
throw new Error('Failed to authenticate or receive a valid token.');
}
} catch (error) {
console.error('An error occurred while loading the project:', error);
alert(error.message);
}
}
// Save Project Function
async function saveProject() {
// Get the current state from the SDK
const currentStateResp = (await window.SDK.document.getCurrentState());
try {
if (currentStateResp.success != true) {
console.log(currentStateResp);
throw new Error("Getting document state failed")
}
// Send the current state data to the server using PUT request
const response = await fetch(`/project/${projectID}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: currentStateResp.data
});
if (!response.ok) {
// If the response is not ok, throw an error
const errorText = await response.text();
throw new Error(`Failed to save project: ${errorText}`);
}
// Handle a successful save
console.log('Project saved successfully!');
} catch (error) {
console.error('An error occurred while saving the project:', error);
alert(error.message);
}
}
async function orderProject() {
await saveProject();
try {
// Send the current state data to the server using PUT request
const response = await fetch(`/order/${projectID}`, {
method: 'POST'
});
if (!response.ok) {
// If the response is not ok, throw an error
const errorText = await response.text();
throw new Error(`Failed to order project: ${errorText}`);
}
// Handle a successful save
alert("Project ordered successfully!");
window.location.href = "/";
} catch (error) {
console.error('An error occurred while ordering the project:', error);
alert(error.message);
}
}
</script>
</body>
</html>
The editor.html
page provided includes three JavaScript functions:
loadProject
: Initializes the editor with the project data on page load.saveProject
: Saves the current state of the project to the server.orderProject
: Finalizes the project and submits an order.
loadProject
This function is responsible for initializing and loading the project into the editor when the web page has finished loading (as it's called by the DOMContentLoaded
event). Here's how it works:
- Initialization: It creates an instance of
StudioSDK
with theeditorId
corresponding to the HTML element where the editor is placed - Authentication: It fetches an authentication token by making an API call (
/user/authdata
). This token is required to ensure that the user has the necessary permissions to load the project. - Configuration: Sets necessary configuration values for the SDK, such as the authentication token and the environment API endpoint.
- Project Loading: The function retrieves the project ID from the current URL path, makes an API call to
/project/${projectID}
to fetch the project data using this ID, and then uses the SDK'sdocument.load
method to load the project data into the editor.
saveProject
This function is invoked when a user clicks the "Save Project" button. Its main goal is to save the current state of the project back to the server:
- State Retrieval: It uses the SDK's
document.getCurrentState
method to get the current state of the project from the editor. - Project Identification: Extracts the project ID from the URL similar to the
loadProject
function. - API Communication: Sends the current state data to the server with a PUT request to the endpoint corresponding to the project ID (
/project/{projectID}
). - Error Handling: If the saving process fails, it throws an error with a message indicating the failure.
Note
This function is intended to be called when a user wants to finalize and order the project which generates an image:
- Prerequisite Action: Calls
saveProject
to ensure the latest state of the project is saved before ordering. - Order Request: Makes a POST request to the
/order/{projectID}
endpoint to create an order for the current project. - Success and Redirection: If successful, alerts the user that the project was ordered and redirects them to the home page.
- Error Handling: In case of an error, it displays an alert with the error message.
For these to work, the following endpoints must be defined:
/user/authdata
: To authenticate the user and retrieve necessary tokens./project/{projectID}
: To handle saving and loading of project data./order/{projectID}
: To send our order to generate an image.