In our fast-paced digital age, where data rules supreme, having a robust and user-friendly file explorer is no longer a luxury but a necessity. Whether you are a developer seeking to streamline your file management tasks or a business owner looking to provide your users with an intuitive file navigation experience, building a File Explorer is a powerful solution. In this ever-evolving tech landscape, harnessing the right tools and frameworks is critical to creating efficient and responsive applications.
Enter Xata and Vue.js – a dynamic duo that promises to revolutionize how you approach file management. Xata, a modern, lightweight database designed for developers, and Vue.js, the progressive JavaScript framework known for its flexibility, combine seamlessly to help you craft a feature-rich File Explorer that is easy to develop and delightful to use.
Let us unlock the potential of Xata and Vue.js as we explore the fusion of Xata and Vue.js in building our own File Explorer that caters to our every need.
GitHub
Check out the complete source code here.
Netlify
We can access the live demo here.
Prerequisite
Understanding this article requires the following:
- Installation of Node.js
- Basic knowledge of JavaScript
- Creating a free account with Xata
Creating a Vue app
We can create a new Vue app using the npm create vue@latest
command.
Scaffolding the project would provide a list of options from which we can select the best fit for the project.
Styling
The CSS framework we will use in this project is Tachyons CSS, which we will install by running the command below in the terminal.
npm i tachyons
Afterward, we will make it globally available for usage in the project by adding the line below in our src/main.js
:
import 'tachyons/css/tachyons.css';
Installing dependencies
We will need some vital dependencies to supercharge our file explorer application. We will install them accordingly using the command below:
npm install --save axios vue-axios @kyvg/vue3-notification vue-pdf-embed
From the above command, we installed the following packages in our application:
- axios: This is a popular JavaScript library for making HTTP requests from the browser. It allows Vue.js applications to interact with APIs and fetch data from external sources
- vue-axios: This is a package that provides integration between Axios and Vue.js. It makes it easier to use Axios in Vue components.
- @kyvg/vue3-notification: This package is a Vue 3 notification plugin that shows notifications or alerts to the user in Vue applications. It is a handy tool for informing users about various events or updates.
- vue-pdf-embed: This package embeds PDF files into Vue.js application. It allows displaying PDF documents within Vue applications, making it useful for a File Explorer where users might want to preview or interact with PDF files.
To use these new dependencies in our application, we will edit our src/main.js
like so:
import Notifications from "@kyvg/vue3-notification";
import axios from "axios";
import VueAxios from "vue-axios";
app.use(VueAxios, axios);
app.use(Notifications);
Setting up the Xata database
We will harness the full potential of Xata as we build out our project. To kick things off, we will either log into our existing Xata account or create a new one. Once inside our Xata account, we will navigate to the database section and establish a fresh database tailored to our project's needs. In our specific case, we have aptly named our database file-explorer
to align with the project's core objectives.
Once our database is in place, our next step involves crafting the essential tables and their corresponding records. We require two distinct tables within our project's scope: the 'users' and 'files' tables. The 'users' table serves as the repository for user information, while the 'files' table is designated to house detailed data about the files we intend to store within the Xata platform.
The media below shows we would create a ‘users’ table in Xata. The same process applies to creating the 'files' table.
The ‘users’ table encompasses records such as usernames, email addresses, and passwords. To add these records to the table, we will follow the process in the media below:
We will also create the relevant records for the files table as shown below:
Links and relationships in Xata
When we upload a file, we want to associate the file with the user responsible for the upload. To do this, Xata provides a record type of link
, which allows us to set up relationships between entities (tables). In other words, we can link the 'users' table to the 'files' table in a one-to-many relationship.
We will have a target table and a source table for this to happen. In our case, the 'files' table will be the source, and the 'users' table will be the target. This arrangement means we target a single user within the 'users' table from the 'files' table.
Here is how we can create a new record in the files table called user
and give it a type of 'link to table':
Setting up Xata in our Vue application
To incorporate Xata into our project, we will follow these installation steps:
Install Xata SDK
We will run the following command in the command line interface:
npx xata
Initialize Xata
Now, we can initialize Xata for use within the application with this command:
xata init
Configuration Options
Xata will present us with various configuration options to choose from. After making our selections, Xata will generate essential files, including .xatrc
and .env
.
Edit API Key
To ensure our Vue application can access the Xata environment, we will need to modify the Xata API key within the Vue app like so:
VITE_XATA_API_KEY="add your api key"
We can now modify the xata.js
file to use this intialised VITE_XATA_API_KEY
like so:
const defaultOptions = {
enableBrowser: true,
apiKey: import.meta.env.VITE_XATA_API_KEY,
databaseURL:
"https://...",
};
We would have seamlessly integrated Xata into our project by following the steps above.
Creating the landing page
First, we will create a basic homepage and progressively enhance it as functionality requires. To do this, we will create a file in views/HomeView.vue
and add the code block below:
<template>
<header class="vh-100 bg-light-pink dt w-100 helvetica">
<div class="background-container dtc v-mid cover ph3 ph4-m ph5-l">
<div class="flex justify-around">
<div>
<h1 class="f2 f-subheadline-l measure lh-title nb1 fw9 white">
Xata File Explorer
</h1>
<h2 class="f6 fw6 white">
Storing and managing files made easy with Xata (File Attachments)
</h2>
</div>
</div>
</div>
</header>
</template>
<style>
.background-container {
background-image: url("../assets/tamanna-rumee-QR56LDZADZ4-unsplash.jpg");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
</style>
Now, when users open the link to the application, the landing page shows the below:
Building the Signup Page
To enable user registration for our application, a signup page is essential. We will create a new file called components/Signup.vue
to achieve this. Then, we will populate it with the following code:
<template>
<form
@submit.prevent="signUp"
class="ba b--white dark-pink bw4 bg-black br2 mw6 center pv2 ph4 shadow-5 f6">
<h2 class="ttc tc">Sign Up</h2>
<label for="name" class="db mb2">Username</label>
<input
id="username"
v-model="username"
name="username"
type="text"
class="db mb3 w-100 br2 pa2 ba bw1 b--black"
placeholder="John Doe" />
<label for="email" class="db mb2">Email</label>
<input
id="email"
v-model="email"
name="email"
type="email"
class="db mb3 w-100 br2 pa2 ba bw1 b--black"
placeholder="example@email.com" />
<label for="password" class="db mb2">Password</label>
<input
id="password"
v-model="password"
name="password"
type="password"
class="db mb3 w-100 br2 pa2 ba bw1 b--black"
placeholder="••••••••" />
<button
type="submit"
class="center db pv2 ph3 mb3 tracked bg-black ba br3 white hover-black hover-bg-dark-pink bg-animate pointer f7">
Sign up
</button>
<p class="f7">
Already have an account?
<a @click="switchToSignin" class="white pointer b">Sign in</a>
</p>
</form>
</template>
With the above, we have effectively established the user interface for our Signup page. Our next focus will be on implementing the essential functionality for authentication and authorization.
We will leverage Xata to achieve our authentication objectives. To integrate Xata into our SignupView, let us start by adding the following code to the components/Signup.vue
file:
<script>
import { getXataClient } from "@/xata";
export default {
name: "Signup",
data: () => ({
username: "",
email: "",
password: "",
}),
methods: {
switchToSignin() {
this.$emit("user-has-account", true);
},
async signUp() {
const xata = getXataClient();
const user = await xata.db.users.filter("email", this.email).getFirst();
if (!user) {
await xata.db.users
.create({
username: this.username,
password: this.password,
email: this.email,
})
.then((res) => {
this.$router.push({
name: "dashboard",
params: { username: res.username },
query: { user: res },
});
this.$notify({
type: "success",
text: "Account creation successful!",
});
});
}
},
},
};
</script>
The code block provided above allowed us to accomplish the following:
- Imported the Xata client, establishing the connection necessary for interacting with our database
- Implemented a mechanism to check for the existence of a user. Our code creates a new user profile if the current user doesn't exist
- Upon successfully creating a user account, the user is seamlessly redirected to the dashboard (which we will create later). Here, the users can access the functionality required to manage their files efficiently
Building the sign-in page
In addition to user registration, providing a seamless sign-in experience for individuals who already possess an account is essential. We will generate a new file named Signin.vue
to achieve this. Within this file, we will insert the code below:
<template>
<form
class="ba f6 b--white bw4 bg-black dark-pink br2 center pa3 shadow-5"
@submit.prevent="signIn">
<h2 class="ttc tc">Sign In</h2>
<label for="email" class="db mb2">Email</label>
<input
id="email"
v-model="email"
name="email"
type="email"
class="db mb3 w-100 br2 pa2 ba bw1 b--black"
placeholder="example@email.com" />
<label for="password" class="db mb2">Password</label>
<input
id="password"
v-model="password"
name="password"
type="password"
class="db mb3 w-100 br2 pa2 ba bw1 b--black"
placeholder="••••••••" />
<button
type="submit"
class="f7 center db pv2 ph3 mb3 tracked bg-black ba br3 white pointer hover-black hover-bg-dark-pink bg-animate pointer">
Sign in
</button>
<p>
Don't have an account?
<a @click="goToSignUp" class="white b f7 pointer">Sign up</a>
</p>
</form>
</template>
The code provided above furnishes us with the foundation for our sign-in interface. However, we must incorporate some key functionalities to ensure it function as intended.
To achieve this, let us add the following code to the <script></script>
section of the SigninView.vue
file:
<script>
import { getXataClient } from "@/xata";
export default {
name: "Signin",
data: () => ({
email: "",
password: "",
}),
methods: {
goToSignUp() {
this.$emit("go-to-sign-up");
},
async signIn() {
const xata = getXataClient();
const user = await xata.db.users.filter("email", this.email).getFirst();
if (!this.email || !this.password) {
this.$notify({ type: "error", text: "Please fill all empty fields" });
} else if (
this.email !== user.email ||
this.password !== user.password
) {
this.$notify({ type: "error", text: "Incorrect credentials" });
this.email = "";
this.password = "";
} else {
this.$router.push({
name: "dashboard",
params: { id: user.id },
});
this.$notify({ type: "success", text: "Login successful!" });
}
},
},
};
</script>
The code block above does the following:
- Imported the Xata client, establishing the connection needed to interact with our database
- Our code checks if the user has provided an email and a password. If either of these is missing, it returns the relevant error message
- Implemented a verification step by comparing the entered email and password with the corresponding records stored in the database
- Upon successful validation, the user is seamlessly redirected to the dashboard. Here, they gain access to the functionality required for managing files effortlessly
To view the sign-in and signup components, we will import them into the views/HomeView.vue
, like so:
<template>
<header class="vh-100 bg-light-pink dt w-100 helvetica">
<div class="background-container dtc v-mid cover ph3 ph4-m ph5-l">
<div class="flex justify-around">
<!-- main code -->
<div class="mv2">
<div v-if="userHasAccount">
<Signin @go-to-sign-up="switchToSignUp" />
</div>
<div v-else>
<Signup @user-has-account="handleUserHasAccount" />
</div>
</div>
</div>
</div>
</header>
</template>
<script>
import Signin from "@/components/Signin.vue";
import Signup from "@/components/Signup.vue";
export default {
components: {
Signin,
Signup,
},
data() {
return {
userHasAccount: false,
};
},
methods: {
handleUserHasAccount(value) {
this.userHasAccount = value;
},
switchToSignUp() {
this.userHasAccount = false;
},
},
};
</script>
The code block above does the following:
- Imported the two child components,
Signin
andSignup
, which handle the sign-in and signup functionality - The
data
property initialises the component'suserHasAccountdata
property asfalse
. This property controls whether the sign-in or signup form should be displayed -
handleUserHasAccount(value)
: This method updates theuserHasAccount
property based on whether the user has an account. It is called when theSignup
component emits theuser-has-account
event -
switchToSignUp()
: This method setsuserHasAccount
tofalse
, switching the view to the signup form
At this point, our interface should look like the below:
Adding upload mechanism
Designing the Upload User Interface
To streamline the process of uploading files, we will create a new file named, components/Uploader.vue
, and then we will add the following code block into the file:
<template>
<div class="flex flex-column flex-row-ns pa3 calisto bg-black-05">
<div class="w-100 ph3">
<form enctype="multipart/form-data" v-if="isInitial || isSaving">
<h2>Upload File</h2>
<div
class="b--dashed bw1 b--light-purple pa3 hover-bg-black-10 bg-animate pointer relative h4">
<input
type="file"
accept="image/*,application/pdf"
class="input-file absolute w-100 h4 pointer o-0" />
<p class="tc f4">
Drag your file here to begin<br />
or click to browse
</p>
</div>
</form>
</div>
</div>
</template>
We achieved the following from the above:
- Implemented a
<form>
element with theenctype="multipart/form-data"
attribute, facilitating file uploads - Incorporated an
input
field with thefile
type attribute, allowing file uploads and restricting the input to accept only pdf and image files using theaccept="
image/*,application/pdf"
attribute
Now, our upload interface should look like the below:
Adding an upload service
To handle the file upload to be displayed in the UI, we will create a utils/file-upload.service.js
and add the code block below:
function upload(formData) {
const photos = formData.getAll('photos');
const promises = photos.map((x) => getImage(x)
.then(img => ({
id: img,
originalName: x.name,
fileName: x.name,
url: img
})));
return Promise.all(promises);
}
function getImage(file) {
return new Promise((resolve, reject) => {
const fReader = new FileReader();
const img = document.createElement('img');
fReader.onload = () => {
img.src = fReader.result;
resolve(getBase64Image(img));
}
fReader.readAsDataURL(file);
})
}
function getBase64Image(img) {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
const dataURL = img.src;
return dataURL;
}
export { upload }
The code above reads the image we will upload, draws it into a canvas, and then converts it to a Base64 string.
Uploading the file to Xata
To upload the file to Xata, we will edit the components/Uploader.vue
to the below:
In the code block above, we achieved the following:
- Represented various component states during file uploading using
STATUS_INITIAL
,STATUS_SAVING
,STATUS_SUCCESS
, andSTATUS_FAILED
definitions, which we initialised in the corresponding computed properties -isInitial
,isSaving
,isSuccess
, andisFailed
-
File Upload Section:
- This section contains a form (
<form>
) withenctype="multipart/form-data"
for file upload - It displays an "Upload File" heading and a drag-and-drop or file input field that allows users to select files
- Depending on the component's state, it shows messages like "Drag your file here to begin" or "Adding file..." to provide user feedback
- When a file is successfully added, it displays a success message and an option to upload another file and previews the uploaded file just by the side
- If the uploaded file is an image, then the
<img />
element will have its:src
attribute populated with the right URL - However, if the file is a pdf, the earlier installed
vue-pdf-embed
package has its:source
filled with the right pdf URL to show the document
- If the uploaded file is an image, then the
- This section contains a form (
-
Component Data:
- The component manages various data properties, including
uploadError
,currentStatus
,fileDataObject
,fileURL
, andfileName
, to track the state and information related to the file uploads and processing
- The component manages various data properties, including
- Methods:
-
reset()
: Resets the form and component's state to its initial state -STATUS_INITIAL
-
save(formData)
: Initiate the fileuploadFileToXata()
to foster uploading to Xata by using theupload
service -
filesChange(fieldName, fileList)
: Handles changes in the selected files and prepares aformData
object for upload -
prepareFormData()
: Processes uploaded file data to create an object with metadata, base64 content, name, and URL for the uploaded image -
uploadFileToXata()
: Uploads the processed file data to a database using the Xata client. Thexata.db.files.create({ … })
takes in theuser
object, which is necessary to link the file we will upload to the user who uploads it
-
At this stage, our user interface will look like this:
Listing uploaded files
We have done a great job by allowing the user upload their files to Xata database. But our file explorer will not be enough if the users cannot view these files and download them if need be. To allow the user view all the files we have uploaded, we will create a new file called, components/Filelist.vue
. In this newly created file, we will add the code below:
<template>
<main class="flex flex-wrap items-center justify-between">
<div v-if="files">
<section
class="w-33-l w-50-m w-100 h-50"
v-for="file in files"
:key="file.id">
<div
class="link dt bb b--black-10 pb2 mt2 blue pointer flex items-center">
<div class="dtc w3">
<div v-if="file.file.mediaType.toLowerCase().includes('image')">
<img :src="file.file.url" alt="Uploaded Image" />
</div>
<div v-else-if="file.file.mediaType.toLowerCase().includes('pdf')">
<img
src="../assets//pdf-placeholder.png"
alt="pdf placeholder Image" />
</div>
</div>
<div class="dtc v-top pl2">
<h1 class="f6 f5-ns fw6 lh-title black mv0">File Name:</h1>
<h2 class="f7 fw4 mt2 mb0 black-60">{{ file.file.name }}</h2>
<a
class="bg-purple dib mv2 f7 pa1 br2 pointer black"
@click="viewFile(file)"
>View File</a
>
</div>
</div>
</section>
</div>
<div v-else class="tc f4">No files uploaded yet</div>
</main>
</template>
<script>
import { getXataClient } from "@/xata";
const xata = getXataClient();
export default {
data() {
return {
files: [],
};
},
components: {
VuePdfEmbed,
},
mounted() {
this.getFilesFromXata();
},
methods: {
async getFilesFromXata() {
const userId = this.$route.params.id;
this.files = await xata.db.files
.filter({ "user.id": userId })
.getMany();
},
},
};
</script>
The code above does the following:
- Vue.js directives like
v-if,
v-for,
andv-else
conditionally render content based on whether there are files to display - Iterates through the list of files and displays each file's details, such as its type (image or PDF), name, and an option to view the file
- The
data
function initializes thefiles
array, storing the list of files to be displayed - In the
mounted
lifecycle hook, the component calls thegetFilesFromXata
method to fetch the list of files associated with the user when the component is mounted - The
getFilesFromXata
method retrieves files associated with a specific user using Xata's database query capabilities. It filters files based on the user's ID and populates thefiles
array with the retrieved data
Our interface should appear like below:
Viewing file content
The implementation above allows users to see a list of all the files they have uploaded to Xata, but it does not cover viewing the file content. To allow users view the uploaded files, we will create a new component called, Fileviewer.vue
and add the code below:
<template>
<div class="overlay">
<!-- Overlay content -->
<div class="overlay-content">
<!-- Close button -->
<span
class="close-button f3 bg-dark-pink white b h1 w1 br3 pa2 center auto flex items-center dim pointer"
@click="closeOverlay"
>X</span
>
<div v-if="fileType === 'image'">
<img :src="fileUrl" alt="File" />
<img
@click="downloadImage"
src="../assets/icon_download.svg"
class="bg-dark-pink pa2 flex items-center br2 white download-button dim" />
</div>
<div v-else-if="fileType === 'pdf'">
<a ref="downloadLink" @click="downloadPdf(fileUrl)">
<img
src="../assets/icon_download.svg"
class="bg-dark-pink pa2 flex items-center br2 white download-button dim" />
</a>
<vue-pdf-embed :source="fileUrl" />
</div>
</div>
</div>
</template>
<script>
import VuePdfEmbed from "vue-pdf-embed";
export default {
props: {
fileUrl: String,
fileType: String,
},
components: {
VuePdfEmbed,
},
methods: {
closeOverlay() {
this.$emit("close-overlay");
},
downloadImage() {
this.$emit("download-image");
},
async downloadPdf(file) {
try {
const response = await this.axios.get(file, {
responseType: "blob", // Set the response type to 'blob' for binary data
});
const blob = new Blob([response.data], { type: "application/pdf" });
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "downloaded.pdf"; // Set the desired file name
link.click();
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Error downloading PDF:", error);
}
},
},
};
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.overlay-content {
background: #fff;
padding: 20px;
max-width: 80%;
max-height: 80%;
overflow: auto;
}
.close-button {
position: absolute;
top: 25px;
right: 30px;
}
.download-button {
position: absolute;
top: 25px;
left: 30px;
}
</style>
- An "X" button for closing the overlay (the
closeOverlay
method is called on click) -
fileType
prop (either 'image' or 'pdf') displays an image or a PDF viewer component - It imports the
VuePdfEmbed
component for displaying PDFs - The component receives two props:
fileUrl
(the file URL to display or download) andfileType
(indicating whether it's an image or PDF) -
closeOverlay()
: This method emits the "close-overlay" event to signal that the overlay should be closed -
downloadImage()
: This method emits the "download-image" event to trigger image download -
downloadPdf(file)
: This method handles PDF download. It uses Axios to fetch the PDF file as binary data, creates a Blob from the response, and generates a downloadable link for the user. If an error occurs during the download process, it's logged to the console
In the Filelist.vue
component, we will import the Fileviewer.vue
and add the necessary methods like so:
<template>
<main class="flex flex-wrap items-center justify-between">
<div v-if="files">
<section
class="w-33-l w-50-m w-100 h-50"
v-for="file in files"
:key="file.id">
<div
class="link dt bb b--black-10 pb2 mt2 blue pointer flex items-center">
<div class="dtc w3">
<!-- codes -->
</div>
<div class="dtc v-top pl2">
<!-- codes -->
<file-viewer
v-if="showOverlay"
ref="goat"
:fileUrl="overlayFileUrl"
:fileType="overlayFileType"
@close-overlay="closeOverlay"
@download-image="downloadImage"></file-viewer>
</div>
</div>
</section>
</div>
</main>
</template>
<script>
import { transformImage } from "@xata.io/client";
import { getXataClient } from "@/xata";
import Fileviewer from "@/components/Fileviewer.vue";
const xata = getXataClient();
export default {
data() {
return {
showOverlay: false,
overlayFileUrl: "",
overlayFileType: "",
};
},
components: {
Fileviewer,
},
mounted() {
this.getFilesFromXata();
},
methods: {
// other methods
viewFile(file) {
this.overlayFileUrl = file.file.url;
file.file.mediaType.toLowerCase().includes("image")
? (this.overlayFileType = "image")
: (this.overlayFileType = "pdf");
this.showOverlay = true;
},
closeOverlay() {
this.showOverlay = false;
},
downloadImage() {
const imageToDownload = transformImage(this.overlayFileUrl, {
download: "image",
format: "jpeg",
});
window.open(imageToDownload, "_blank");
},
},
};
</script>
We achieved the following using the code block above:
- Imported
transformImage
from "@xata.io/client" andgetXataClient
from "@/xata" -
viewFile(file)
: When a user clicks on a file, this method is called. It sets theoverlayFileUrl
to the URL of the clicked file, determines the file type (image or PDF) based on its media type, and setsshowOverlay
to true to display the overlay -
closeOverlay()
: This method is called when the user wants to close the overlay. It setsshowOverlay
to false, hiding the overlay -
downloadImage()
: This method is called when the user wants to download an image. It transforms the image using Xata'stransformImage
function, then opens a new browser window to download the transformed image
We should have our UI looking like this at this stage:
Now we will create a new file called, views/DashboardView.vue
which will be our dashboard. In this file, we will import the components/Uploader.vue
and the components/Filelist.vue
file, like so:
<template>
<div class="bg-light-purple vh-100 dt w-100 helvetica pv2 ph3">
<div class="flex justify-between items-center">
<h1 class="ttc">Xata File Explorer</h1>
<span class="b db underline-hover pointer" @click="signOut"
>Sign out</span
>
</div>
<Uploader />
</div>
</template>
<script>
import Uploader from "@/components/Uploader.vue";
export default {
components: {
Uploader,
},
methods: {
signOut() {
this.$router.push({ name: "home" });
this.$route.params.id = "";
this.$notify({ type: "success", text: "User logged out!" });
},
},
};
</script>
In the code above, we achieved the following:
- Created a new file called
views/DashboardView.vue
- Imported the
Upload
component - Created a
signOut
method that logs users out
At this point, our file explorer application built with Xata should look like the below:
Conclusion
In this article, we talked about building a File Explorer using Xata. This project exemplifies the capabilities of Xata and Vue.js and the amazing tools we can build from combining these two tools together or individually. The File Explorer we built is the beginning of what we can achieve.
Resources
Here are some additional resources that will be helpful when working with Xata:
Top comments (2)
Wow .....Brovo .... in fact i fall in love with the code and the documentation straight away 🙌🙌🙌
Thanks for the feedback. I am glad you found it helpful.