With the current digital era and technological improvement, it has become easier to purchase, sell, and rent properties. Building a real estate listing application is one potential gold mine for entrepreneurs and developers who hope to capitalize on the real estate market.
In this guide, we will build a real estate listing application using Strapi and SvelteKit.
To follow along with this tutorial, ensure you meet the following prerequisites.
SvelteKit is a robust framework for creating fast, reliable, and reactive web applications. Its server-side rendering allows you to quickly build SEO-friendly apps, which is essential for online businesses.
In this comprehensive SvelteKit tutorial, we will cover everything from setting up SvelteKit, SvelteKit authentication, persisting data in SvelteKit using a store, to adding Tailwind CSS to SvelteKit, and many more. This thorough coverage will give you the confidence to master SvelteKit.
You can read on the SvelteKit docs to learn more.
Strapi is a free and open-source content management system (CMS) built with Node.js.
It's used in web development to create, manage, and expose content-rich experiences to any digital device.
With what we know so far, let's build a Real Estate Listing Platform!
Once you meet the requirement above, create a main folder, real-estate,
to house the backend and frontend of our application. In the real-estate
folder run the command below to bootstrap a new Strapi project:
cd real-estat
npx create-strapi-app@latest real-estate-back --quickstart
Start the project by running the command below:
npm run develop
This will start a Strapi instance on port 1337
. If it does not automatically open the page, type localhost:1337
in the browser bar to launch the panel:
Create a new Strapi admin user and login to start adding content.
Adding content with Strapi is a straight forward process. All you need is to create a collection type and seed it with some data.
In this tutorial, we created one collection type Estate
having 6 fields:
imageurl
: Media (single media). This represents the image of the property. property_name
: Text (short text). This is the property name.location
: Text (short text). This is the place where the estate is locate. status
: Text (short text). This field indicates if the estate is for sale or for rent. price
: Number. This attribute is the selling/renting price Of the property desc
: Text (long text). And finally we have the description of the property. Don’t forget to mark some or all attributes as required
.
Once you have created your content type and populated it with some data, you will move to the next step which consists of creating a Sveltekit Project.
Another last point is to setup roles and permissions for public users so that the can find
and findOne
estate:
Go back to thereal-estate
directory you created earlier and run the following command to create a new SvelteKit project:
npm create svelte@latest real-estate-front
Be sure to choose the options as shown in the below image.
Install project dependencies:
cd real-estate-front
npm install
Once the dependencies are installed, you can now run the it with the command below:
npm run dev
Looking at the structure of the project, we have the routes
and lib
folder in the src
with the app/html
file at the root.
Every file in the src/routes
folder will be mapped to actual SvelteKit routes. For example, src/routes/+page.svelte
will be accessible at /
, src/routes/about/+page.svelte
will be accessible at /about
and src/routes/estate/[id]/+page.svelte
will be accessible via /estate/${id}/
where id
is a number or ID of an estate listing. This is how filesystem-based routing generally works in SvelteKit. This is also called SvelteKit routes.
In the src/routes
you can also find the +layout.svelte
which is a special file that adds a layout to every page. You can equally apply a layout to subdirectories. Learn more about Sveltekit layout here
We will create 5 main folders in the src/routes
folder:
about
folder which will hold informtion for the about page about/+page.svelte
create
folder which will hold the file for creating an estate create/+page.svelte
estate
folder for the details page of an estate. Here you will create an[id]
inside which will nest the +page.svelte
so it will be estate/[id]/+page.svelte
login
folder login/+page.svelte
update
folder. Create the update
folder. Inside it, create another folder, [id]
. Then inside the [id]
folder, create the page file +page.svelte
.You will use 3 packages in this project:
npm install axios
npm install svelte-persisted-store
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Add the paths to all your template files:
1content: ['./src/**/*.{html,js,svelte,ts}'],
Add the @tailwind
directives in the src/routes/syle.css
file to finish setting up our SvelteKit Tailwind CSS configuration:
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
Once both projects are setup, you can get to the interesting part of the tutorial which consists of creating and calling API requests, dealing with pagination, and user authentication.
Axios is a popular JavaScript library used to make HTTP requests from Node.js or XMLHttpRequests from the browser. It is often employed in web development to interact with APIs and perform CRUD operations (Create, Read, Update, Delete).
Inside the src/lib
folder, create a file app.ts
to start creating the various requests in the code below in the src/lib/api.ts
file:
1import axios from "axios";
2
3const api = axios.create({
4 baseURL: "http://localhost:1337/api",
5});
6
7// Get an estate by id
8export const getEstateById = async (id: string) => {
9 try {
10 const response = await api
11 .get(`/estates/${id}?populate=*`)
12 .then((res) => (res?.data ? res.data.data : []));
13
14 return response;
15 } catch (error) {
16 console.error("Error fetching estate:", error);
17 throw new Error("Server error");
18 }
19};
20
21// delete an estate
22export async function deleteEstateById(id: string) {
23 try {
24 const response = await api.delete(`/estates/${id}?populate=*`);
25 return response.status === 200;
26 } catch (error) {
27 console.error("Failed to delete estate:", error);
28 return false;
29 }
30}
31
32// create an estate
33export function createEstate(estateData: FormData) {
34 return api.post("/estates?populate=*", estateData);
35}
36
37// Update an estate
38export function updateEstate(id: string, estateData: FormData) {
39 return api.put(`/estates/${id}?populate=*`, estateData);
40}
41
42// get list of estate order by creation date and page limit
43export async function getPaginatedEstates(page = 1, limit = 5) {
44 try {
45 const response = await api.get("/estates?populate=*", {
46 params: {
47 pagination: {
48 page: page,
49 pageSize: limit,
50 },
51 sort: "createdAt:desc",
52 },
53 });
54 return {
55 data: response.data,
56 pagination: response.data.meta.pagination,
57 };
58 } catch (error) {
59 console.error("Error fetching paginated estates:", error);
60 return {
61 data: [],
62 pagination: {page: 1, pageSize: limit, pageCount: 0, total: 0},
63 };
64 }
65}
66export default api;
api
is the instance of Axios named with a base URL pointing to localhost:1337/api
.getEstateById
retrieves an estate by its ID and sends a GET
request to /estates/${id}
which populates the parameter to get associated data.deleteEstateById
deletes an estate given its ID.createEstate
takes in estateData
as FormData
to create a new entry.updateEstate
updates an estate listing by sending a PUT
request to the given URL with the necessary data.getPaginatedEstates
functions retrieves a list of estates, ordered by date of creation. It sends a GET
request with pagination parameters.From here you can proceed to creating the home page.
You will display the list of properties in the home page which correspond to your src/routes/+page.svelte
:
1<script lang="ts">
2 import {onMount} from "svelte";
3 import {getPaginatedEstates} from "$lib/api";
4
5 let estates: any[] = [];
6 let currentPage = 1;
7 let pageSize = 5;
8 let pageCount = 0;
9 let totalEstates = 0;
10
11 async function loadEstates() {
12 try {
13 const result = await getPaginatedEstates(currentPage, pageSize);
14
15 if (result.data.data && Array.isArray(result.data.data)) {
16 estates = result.data.data;
17 if (result.pagination) {
18 pageSize = result.pagination.pageSize;
19 pageCount = result.pagination.pageCount;
20 totalEstates = result.pagination.total;
21 } else {
22 console.error("Pagination data is missing");
23 }
24 } else {
25 throw new Error("Data is not an array or undefined");
26 }
27 } catch (error: any) {
28 console.error("Failed to fetch estates:", error.message);
29 // Set to empty array on error
30 estates = [];
31 }
32 }
33
34 function goToPage(page: number) {
35 if (page > 0 && page <= pageCount) {
36 currentPage = page;
37 loadEstates();
38 }
39 }
40
41 // Initially load estates
42 onMount(() => {
43 loadEstates();
44 });
45</script>
46
47<h1 class="text-center text-3xl font-bold text-blue-500">
48 Real-Estate Listing
49</h1>
50
51<div class="my-4 flex justify-between items-center">
52 <nav class="flex items-center">
53 <button
54 class="mr-10 bg-gray-300 rounded px-3 py-2 text-[12px]"
55 on:click={() => goToPage(currentPage - 1)}
56 disabled={currentPage <= 1}>← Previous</button
57 >
58 <button
59 class=" bg-gray-300 rounded px-3 py-2 text-[12px]"
60 on:click={() => goToPage(currentPage + 1)}
61 disabled={currentPage >= pageCount}>Next →</button
62 >
63 </nav>
64
65 <a
66 href="/create"
67 class="bg-blue-500 rounded px-5 py-2 hover:bg-blue-700 text-white font-bold"
68 >Add Properties</a
69 >
70</div>
71
72<main>
73 {#if estates}
74 <div class="grid grid-cols-3 mb-4 gap-10">
75 {#each estates as estate}
76 <a href={`/estate/${estate.id}`} class="mb-5 mt-10">
77 <div class="img_content mb-5">
78 <img
79 src={`http://localhost:1337${estate.attributes.imageUrl.data?.attributes.url}`}
80 alt={estate.attributes.imageUrl.data.alternativeText}
81 />
82 </div>
83 <p class="text-blue-500 font-bold">$ {estate.attributes.price}</p>
84 <p class="text-blue-500">{estate.attributes.property_name}</p>
85 <p class="text-blue-500">{estate.attributes.status}</p>
86 <p class="text-blue-500">At {estate.attributes.location}</p>
87 </a>
88 {/each}
89 </div>
90 {/if}
91</main>
92
93<style>
94 .img_content {
95 background-color: blue;
96 position: relative;
97 height: 300px;
98 width: 300px;
99 }
100
101 .img_content img {
102 object-fit: cover;
103 height: 300px;
104 width: 300px;
105 }
106</style>
The Svelte script above fetches paginated real estate listings from the API getPaginatedEstates
, then handles navigation between pages with goToPage
, and displays properties. It retrieves estates based on the currentPage
and displays them dynamically. The Navigation buttons Previous
and Next
allow you to move between pages. However, if an error occurs during fetching, it handles it gracefully by displaying an empty list.
Svelte's onMount
function executes a callback when the component is mounted to the DOM. loadEstates()
function is invoked essentially to trigger the initial loading of estates which fetches data from the API and populates the estates
.
Now we would like to have more information about a property before proceeding with buying or renting.
To proceed, add the code below in the src/routes/estate/[id]/+page.svelte
file:
1<script lang="ts">
2 import {getEstateById} from "$lib/api.js";
3 import {page} from "$app/stores";
4 import {onMount} from "svelte";
5
6 let estateId = $page.params.id;
7 export let estate: any;
8 onMount(async function () {
9 const estates = await getEstateById(estateId);
10 if (estates) {
11 estate = estates;
12 } else {
13 estate = {};
14 }
15 });
16</script>
17
18{#if estate?.attributes}
19 <div class="mt-5 mb-20">
20 <h1 class="mb-3">{estate.attributes.property_name}</h1>
21 <div class="img_content mb-5 mx-auto">
22 <img
23 src={`http://localhost:1337${estate.attributes.imageUrl.data?.attributes.url}`}
24 alt={estate.attributes.imageUrl.data.alternativeText}
25 />
26 </div>
27 <p class="text-red-500">
28 Price: $<span class="font-bold">{estate.attributes.price}</span>
29 </p>
30 <p class="text-red-500">
31 Status: <span class="font-bold"> {estate.attributes.status}</span>
32 </p>
33 <p class="text-red-500">
34 Location: <span class="font-bold"> {estate.attributes.location}</span>
35 </p>
36
37 <div class="mt-10">{estate.attributes.desc}</div>
38 </div>
39{/if}
40
41<style>
42 .img_content {
43 background-color: blue;
44 position: relative;
45 height: 500px;
46 width: 100%;
47 }
48
49 .img_content img {
50 object-fit: cover;
51 height: 500px;
52 width: 100%;
53 }
54</style>
Here is what the code above does:
getEstateById
fetches estate details and If the estate data exists, it assigns it to the estate variable; otherwise, it initializes estate as an empty object and conditionally renders estate details only if estate.attributes exist.
Clicking on an estate on the home page will direct you to localhost:5173/estate/id
. id
here correspond to the ID of the page property where you will get the property details like the property name, image, price, status, location, and description.
When you click on an estate on the home page, it will direct you to localhost:5173/estate/id
corresponding to the ID of the property, where you will display the property details such as the property name, image, price, status, location, and description.
This operation consists of removing a property from the database. Edit the src/routes/+page.svelte
like below
1 <script lang="ts">
2 import {deleteEstateById} from "$lib/api";
3 ...
4
5
6 ...
7 async function handleDelete(id: string) {
8 try {
9 const isSuccess = await deleteEstateById(id);
10 if (isSuccess) {
11 estates = estates.filter((estate) => estate.id !== id);
12 } else {
13 console.error("Failed to delete estate");
14 }
15 } catch (error) {
16 console.error("Error deleting estate:", error);
17 }
18 }
19 ...
20
21</script>
22
23 <main>
24 {#if estates}
25 <div class="grid grid-cols-3 mb-4 gap-10">
26 {#each estates as estate}
27 <a href={`/estate/${estate.id}`} class="mb-5 mt-10">
28 <div class="img_content mb-5">
29 <img
30 src={`http://localhost:1337${estate.attributes.imageUrl.data?.attributes.url}`}
31 alt={estate.attributes.imageUrl.data.alternativeText}
32 />
33 </div>
34 <p class="text-blue-500 font-bold">$ {estate.attributes.price}</p>
35 <p class="text-blue-500">{estate.attributes.property_name}</p>
36 <p class="text-blue-500">{estate.attributes.status}</p>
37 <p class="text-blue-500">At {estate.attributes.location}</p>
38
39 <!--delete estate-->
40
41 <div class="grid grid-cols-2 mt-2 text-center gap-2">
42 <a
43 href=""
44 on:click|preventDefault={() => handleDelete(estate.id)}
45 class="bg-red-500 rounded px-5 py-2 hover:bg-red-700 text-white font-bold"
46 >Delete</a>
47 </div>
48 </a>
49 {/each}
50 </div>
51
52 {/if}
53
54 </main>
From the snippet above, handleDelete
tries to delete the estate by calling the deleteEstateById
function from the API. It awaits the result of the deletion operation. If the deletion operation is successful (isSuccess
is true), it updates the estates
array by filtering out the deleted estate based on its ID, else it logs an error message to the console to the user.
If you would like to give the possibility to a user to add a property for listing in the frontend, you will use the createEstate()
to add new entries. In the src/routes/create/+page.svelte
, add the following code in your:
1<script lang="ts">
2 import {goto} from "$app/navigation";
3 import { createEstate } from "$lib/api";
4
5 let property_name = "";
6 let status = "";
7 let price: number;
8 let desc = "";
9 let location = "";
10 let file: FileList;
11
12 async function submitEstate() {
13 const formData = new FormData();
14 if (file && file.length > 0) {
15 // Append the first file
16 formData.append("files.imageUrl", file[0]);
17 }
18 formData.append(
19 "data",
20 JSON.stringify({property_name, status, price, desc, location})
21 );
22
23 try {
24 await createEstate(formData);
25 // Redirect to home after submission
26 goto("/");
27 } catch (error: any) {
28 console.error("Error creating estate", error.response);
29 }
30 }
31</script>
32
33<div class="w-full mx-auto mt-5">
34 <div class="mb-5 text-center">
35 <p>Use the form below to add a new property</p>
36 </div>
37
38 <form
39 on:submit|preventDefault={submitEstate}
40 class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
41 >
42 <div class="mb-4">
43 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
44 Property Image
45 </label>
46 <input
47 bind:files={file}
48 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
49 id=""
50 type="file"
51 placeholder="Property Image"
52 />
53 </div>
54 <div class="mb-4">
55 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
56 Property Name
57 </label>
58 <input
59 bind:value={property_name}
60 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
61 id=""
62 type="text"
63 placeholder="Property Name"
64 />
65 </div>
66 <div class="mb-6">
67 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
68 Location
69 </label>
70 <input
71 bind:value={location}
72 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
73 id=""
74 type="text"
75 placeholder="Status"
76 />
77 </div>
78 <div class="mb-6">
79 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
80 Status
81 </label>
82 <input
83 bind:value={status}
84 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
85 id=""
86 type="text"
87 placeholder="Status"
88 />
89 </div>
90 <div class="mb-6">
91 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
92 Price in USD
93 </label>
94 <input
95 bind:value={price}
96 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
97 id=""
98 type="number"
99 placeholder="Price"
100 />
101 </div>
102 <div class="mb-6">
103 <label class="block text-gray-700 text-sm font-bold mb-2" for="">
104 Description
105 </label>
106 <textarea
107 bind:value={desc}
108 rows="5"
109 class="shadow appearance-none border resize-none rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
110 id=""
111 placeholder="Description"
112 ></textarea>
113 </div>
114 <div class="text-center">
115 <button
116 class="bg-blue-500 w-full hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
117 type="submit"
118 >
119 Add
120 </button>
121 </div>
122 </form>
123 </div>
In the code snippet above, the property_name
, status
, price
, desc
, location
, and file
store the property details. The file
represents the estate image property. The submitEstate
function creates an object(Formdata
) that handles file uploads and appends property details to it on submission.
createEstate
is used to send a POST
request to the API endpoint /estates
to add the new property.
The same is true if you try to create an estate. You will get a 403
error since only authenticated users can create a property. If you will like to test the createEstate()
right away, you can you can edit the user permissions and roles.
So far, you have seen how to read, delete and create a property with SvelteKit and Strapi, in the next part you will learn how to update an existing property.
Updating a property is quite similar to creating one except that instead of using a POST
request, you will use a PUT
request. In your src/routes/update/[id]/+page.svelte
file, add the code below:
1<script lang="ts">
2 import {onMount} from "svelte";
3 import {page} from "$app/stores";
4 import {getEstateById, updateEstate} from "$lib/api";
5 import {goto} from "$app/navigation";
6
7 let selectedFile: File | null = null;
8
9 let estateId = $page.params.id;
10 let estate = {
11 property_name: "",
12 status: "",
13 file: FileList,
14 price: "",
15 location: "",
16 desc: "",
17 };
18
19 // Load the estate data when the component mounts
20 onMount(async () => {
21 const response = await getEstateById(estateId);
22 if (response.attributes) {
23 estate = {...response.attributes};
24 }
25 });
26
27 // Handle form submission for update
28 async function handleSubmit() {
29 const formData = new FormData();
30 formData.append(
31 "data",
32 JSON.stringify({
33 property_name: estate.property_name,
34 status: estate.status,
35 price: estate.price,
36 location: estate.location,
37 desc: estate.desc,
38 })
39 );
40 if (estate.file instanceof File) {
41 formData.append("files.imageUrl", estate.file, estate.file.name);
42 }
43
44 try {
45 await updateEstate(estateId, formData);
46
47 goto("/");
48 } catch (error) {
49 console.error("Error updating estate:", error);
50 }
51 }
52
53 function handleFileChange(event: Event) {
54 // Correctly type the event target
55 const input = event.target as HTMLInputElement;
56 if (input.files && input.files.length > 0) {
57 // Access the first file
58 selectedFile = input.files[0];
59 } else {
60 // No file selected
61 selectedFile = null;
62 }
63 }
64</script>
65
66 <div class="w-full mx-auto mt-5">
67 <div class="mb-5 text-center">
68 <p>Use the form below to edit {estate.property_name}</p>
69 </div>
70
71 <form
72 on:submit|preventDefault={handleSubmit}
73 class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
74 >
75 <div class="mb-4">
76 <label
77 class="block text-gray-700 text-sm font-bold mb-2"
78 for="propertyImage"
79 >
80 Property Image
81 </label>
82 <input
83 on:change={handleFileChange}
84 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
85 id="propertyImage"
86 type="file"
87 placeholder="Property Image"
88 />
89 </div>
90 <div class="mb-4">
91 <label
92 class="block text-gray-700 text-sm font-bold mb-2"
93 for="propertyName"
94 >
95 Property Name
96 </label>
97 <input
98 bind:value={estate.property_name}
99 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
100 id="propertyName"
101 type="text"
102 placeholder="Property Name"
103 />
104 </div>
105 <div class="mb-6">
106 <label
107 class="block text-gray-700 text-sm font-bold mb-2"
108 for="propertyLocation"
109 >
110 Location
111 </label>
112 <input
113 bind:value={estate.location}
114 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
115 id="propertyLocation"
116 type="text"
117 placeholder="Status"
118 />
119 </div>
120 <div class="mb-6">
121 <label
122 class="block text-gray-700 text-sm font-bold mb-2"
123 for="propertyStatus"
124 >
125 Status
126 </label>
127 <input
128 bind:value={estate.status}
129 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
130 id="propertyStatus"
131 type="text"
132 placeholder="Status"
133 />
134 </div>
135 <div class="mb-6">
136 <label
137 class="block text-gray-700 text-sm font-bold mb-2"
138 for="propertyPrice"
139 >
140 Price in USD
141 </label>
142 <input
143 bind:value={estate.price}
144 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
145 id="propertyPrice"
146 type="number"
147 placeholder="Price"
148 />
149 </div>
150 <div class="mb-6">
151 <label
152 class="block text-gray-700 text-sm font-bold mb-2"
153 for="propertyDesc"
154 >
155 Description
156 </label>
157 <textarea
158 bind:value={estate.desc}
159 rows="5"
160 class="shadow appearance-none border resize-none rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
161 id="propertyDesc"
162 placeholder="Description"
163 ></textarea>
164 </div>
165 <div class="text-center">
166 <button
167 class="bg-blue-500 w-full hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
168 type="submit"
169 >
170 Update
171 </button>
172 </div>
173 </form>
174 </div>
From the code above, you will find the following
estateId
is the ID of the estate. onMount
asynchronously loads the details of a property on component mount and stores them in the estate variable.handleSubmit
builds an object(FormData
) containing the updated estate details and then sends a PUT
request to the server to update the estate details with the updateEstate
function from the api.ts
. A positive response redirects you to the home page otherwise it logs into the console an error message. handleFileChange
function that updates file
variable with the selected file. And once again, the input fields are bound to the respective properties of the estate
variable. To associate this function with the estate and open the edit page whenever you click on an estate on the the homepage, you need to make some adjustments in the src/routes/+page.svelte
file like below:1<script>
2...
3import { goto } from "$app/navigation";
4
5
6 // Function to handle page changes
7
8 function editProperty(id: string) {
9 // Navigate to the edit form with id
10 goto(`/update/${id}`);
11 }
12
13...
14</script>
15...
16
17
18
19<div class="grid grid-cols-2 mt-2 text-center gap-2">
20
21 <!--update estate-->
22
23 <a href={`/update/${estate.id}`}
24 on:click={() => editProperty(estate.id)}
25 class="bg-blue-500 rounded px-5 py-2 hover:bg-blue-700 text-white font-bold"
26 >Update</a>
27
28 <!--delete estate-->
29
30 <a href=""
31 on:click|preventDefault={() => handleDelete(estate.id)}
32 class="bg-red-500 rounded px-5 py-2 hover:bg-red-700 text-white font-bold"
33 >Delete</a>
34</div>
When you click on the "Update" button, it calls the editProperty
function, which redirects you to the edit form we created earlier with the initial values of the estate.
You will not want anyone to just access your application and create an estate or delete one at any moment. You may prefer to give access only to authenticated users. To proceed, set authentication and authorization so that only verified users can have access to functions like, delete, update or create. A simple user can only view the list of estate.
Strapi Users & Permissions plugin is enabled by default and handles user registration, login, and permission settings.
Configure Roles & Permissions in the Strapi admin panel to get started. Navigate to Settings > USERS & PERMISSIONS PLUGIN > Roles.
Here, you can define roles (like "Authenticated") and set permissions for each role concerning what API endpoints they can access.
We want an authenticated user to be able to upload files, create, update and delete an estate.
But we don’t want anyone to create an account in the application and start using it. Instead, we only want a specific group of users. So you will need to manually create a user in the Strapi panel and build a login page in the Sveltekit app for authorized users to log in, create, update or delete a property.
Note: Strapi allows you to configure third-party providers like Google, LinkedIn, Instagram, Facebook and more but in this tutorial we will stick with the email/password method.
In the Strapi admin navigate to the Content Manager → Content Types → Users → Create New Entry to add a new user
Provide the name, email and password of the user and set his role to authenticate.
Once everything is setup you can start creating the login page. But before that, you should create a store that will help to persist user data to local storage and sync changes across browser tabs.
Create authStore.ts
file in the lib
directory and add the code below:
1import {persisted} from "svelte-persisted-store";
2import type {Writable} from "svelte/store";
3interface AuthStoreData {
4 isAuthenticated: boolean;
5 user: {
6 id: number;
7 username: string;
8 email: string;
9 } | null;
10 token: string | null;
11}
12
13// manage auth state
14export const authStorePersist: Writable<AuthStoreData> = persisted("strapi", {
15 isAuthenticated: false,
16 user: null,
17 token: null,
18});
19
20// update an authentication store with new user data
21export function setAuth(data: {user: AuthStoreData["user"]; jwt: string}) {
22 authStorePersist.set({
23 isAuthenticated: true,
24 user: data.user,
25 token: data.jwt,
26 });
27}
28// mark user as not authenticated and clear the user details and token
29export function clearAuth() {
30 authStorePersist.set({
31 isAuthenticated: false,
32 user: null,
33 token: null,
34 });
35}
In the code snippet above, we defined the structure of the authentication store data and created a writable Svelte store named authStorePersist
using the persisted
function from the svelte-persisted-store
package which holds the authentication state data and persists it to local storage with the key strapi
The setAuth
function updates the authentication store with new user data and a JWT
token, then sets isAuthenticated
to true, assigns the user
and token
properties with the provided data, and updates the store using authStorePersist.set()
.
The clearAuth
function marks the user as unauthenticated and clears the user's token then updates the store again using authStorePersist.set()
.
In the api.ts
file, add the code below:
1import {authStorePersist, clearAuth, setAuth} from "./authStore";
2import {get} from "svelte/store";
3
4...
5
6//send and set user credentials
7export async function login(email: string, password: string) {
8 try {
9 const response = await api.post("/auth/local", {
10 identifier: email,
11 password,
12 });
13 setAuth(response.data);
14 return response.data;
15 } catch (error: any) {
16 console.error("Login error:", error.response);
17 throw error;
18 }
19}
20
21// Clear authentication state
22export async function logout() {
23 clearAuth();
24}
25
26api.interceptors.request.use((config) => {
27 const store = get(authStorePersist);
28 if (store.isAuthenticated) {
29 config.headers.Authorization = `Bearer ${store.token}`;
30 }
31 return config;
32});
login
function sends a request to the /auth/local
endpoint and if the response is positive, it sets the user's authentication state using the setAuth function from the store and returns the response data or returns an error.logout
function calls the clearAuth
function while the API Interceptor intercepts all outgoing requests made using the Axios API instance. Before sending a request, it retrieves the authentication state from the store and checks if the user is authenticated. If it’s the case, it adds an Authorization header with a Bearer token derived from the stored token. This ensures that authenticated users can access protected API endpoints.
Now create a login page where the user will enters his crendentials and be able to perform CRUD operations. Create the login/+page.svelte
folder and insert this code:
1<script lang="ts">
2 import {goto} from "$app/navigation";
3 import {login} from "$lib/api";
4 let email = "";
5 let password = "";
6 let errorMessage = "";
7
8 async function handleLogin() {
9 try {
10 await login(email, password);
11
12 goto("/");
13 } catch (error: any) {
14 errorMessage = error.message;
15 }
16 }
17</script>
18
19<div class="w-full max-w-xs mx-auto mt-20">
20 <form
21 on:submit|preventDefault={handleLogin}
22 class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
23 >
24 <div class="mb-4">
25 <label class="block text-gray-700 text-sm font-bold mb-2" for="username">
26 Email
27 </label>
28 <input
29 bind:value={email}
30 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
31 id="email"
32 type="email"
33 placeholder="Email"
34 />
35 </div>
36 <div class="mb-6">
37 <label class="block text-gray-700 text-sm font-bold mb-2" for="password">
38 Password
39 </label>
40 <input
41 bind:value={password}
42 class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
43 id="password"
44 type="password"
45 placeholder="******************"
46 />
47 </div>
48 <div class="text-center">
49 <button
50 class="bg-blue-500 w-full hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
51 type="submit"
52 >
53 Sign In
54 </button>
55 {#if errorMessage}
56 <p style="color: red;">{errorMessage}</p>
57 {/if}
58 </div>
59 </form>
60</div>
Everything is now setup but there is one more last checkup to make to complete the application and prevent unauthorized users from accessing certain resources.
On the home page, we only want authenticated users to create, update and delete a property. So adjust your src/routes/+page.svelte
with the code below:
1import { authStorePersist } from "$lib/authStore";
2
3...
4
5import {authStorePersist} from "$lib/authStore";
6
7 // initialize with the current value
8 let storeValue: any;
9
10 // subscribe to changes in the store
11 authStorePersist.subscribe((value) => {
12 // update the local variable with the new value
13 storeValue = value;
14 });
15
16 ...
17
18 <div class="my-4 flex justify-between items-center">
19 <nav class="flex items-center">
20
21 ...
22
23 </nav>
24
25 {#if storeValue.isAuthenticated}
26 <a
27 href="/create"
28 class="bg-blue-500 rounded px-5 py-2 hover:bg-blue-700 text-white font-bold"
29 >Add Properties</a
30 >
31 {/if}
32</div>
33
34...
35
36{#if storeValue.isAuthenticated}
37 <div class="grid grid-cols-2 mt-2 text-center gap-2">
38 <a
39 href={`/update/${estate.id}`}
40 on:click={() => editProperty(estate.id)}
41 class="bg-blue-500 rounded px-5 py-2 hover:bg-blue-700 text-white font-bold"
42 >Update</a>
43 <a
44 href=""
45 on:click|preventDefault={() => handleDelete(estate.id)}
46 class="bg-red-500 rounded px-5 py-2 hover:bg-red-700 text-white font-bold"
47 >Delete</a>
48 </div>
49{/if}
50
51...
Only authenticated users can view the create property form. src/routes/create/+page.svelte
:
1 import {authStorePersist} from "$lib/authStore";
2 // initialize with the current value
3 let storeValue: any;
4
5 // subscribe to changes in the store
6 authStorePersist.subscribe((value) => {
7 // update the local variable with the new value
8 storeValue = value;
9 });
10
11 ...
12
13{#if storeValue.isAuthenticated}
14 <div class="w-full mx-auto mt-5">
15 ...
16 </div>
17{:else}
18 <div class="text-center mt-20">
19 <p class="mb-5">
20 You are not Currently logged, To create a property please log in
21 </p>
22 <a href="/login"
23 class="bg-blue-500 rounded mt-3 px-5 py-2 hover:bg-blue-700 text-white font-bold"
24 >Login</a>
25 </div>
26{/if}
Only authenticated users can update a property. src/routes/update/[id]/+page.svelte
1...
2import {authStorePersist} from "$lib/authStore";
3import {get} from "svelte/store";
4
5let storeValue;
6
7// initialize with the current value
8$: storeValue = get(authStorePersist);
9
10...
11
12{#if storeValue.isAuthenticated}
13 <div class="w-full mx-auto mt-5">
14 <div class="mb-5 text-center">
15 <h1 class="text-center">
16 Welcome <span class="text-blue-500 capitalize"
17 >{storeValue.user?.username}</span
18 >
19 </h1>
20 <p>Use the form below to edit {estate.property_name}</p>
21 </div>
22
23 <form
24 on:submit|preventDefault={handleSubmit}
25 class="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
26 >
27 ...
28
29 </form>
30 </div>
31{/if}
In the Header, remove this part in the nav
:
1<li aria-current={$page.url.pathname.startsWith('/sverdle') ? 'page' : undefined}>
2 <a href="/sverdle">Sverdle</a>
3</li>
If a user is authenticated, display the "Log Out" button, else display the "Login" button:
1<script lang="ts">
2 ...
3 import {onMount} from "svelte";
4 import {logout} from "$lib/api";
5 import {authStorePersist} from "$lib/authStore";
6
7 onMount(async () => {});
8
9 const handleLogout = () => {
10 logout();
11 };
12
13 let storeValue: any; // initialize with the current value
14
15 // subscribe to changes in the store
16 authStorePersist.subscribe((value) => {
17 // update the local variable with the new value
18 storeValue = value;
19 });
20</script>
21
22<header>
23
24 <nav>
25
26 ...
27
28 </nav>
29 <div class="corner">
30 {#if storeValue.isAuthenticated}
31 <button
32 type="submit"
33 on:click={handleLogout}
34 class="text-blue-500 rounded focus:outline-none focus:shadow-outline"
35 >
36 Logout
37 </button>
38 {:else}
39 <a
40 href="/login"
41 class="text-blue-500 px-6 rounded focus:outline-none focus:shadow-outline"
42 >
43 Login
44 </a>
45 {/if}
46 </div>
47</header>
Finally, test your application by running the backend:
1cd real-estate-back
2npm run developer
and running the frontend:
1cd real-estate=front
2npm run dev
Athenticated user preview:
We have also prepared a demo so that you can actually visualize what you are about to create.
In this Strapi SvelteKit tutorial, you looked at how to build a real estate listing application with SvelteKit alongside Strapi CMS as the backend. You set up Strapi for new users, then proceeded to make the front using SvelteKit and integrated functions like user authentication, property listing, creation, edition, and deletion.
You can elevate this application to the next level by incorporating other features, such as search and filters, animation, and loading states.