DEV Community

Cover image for Build a streaming drag and drop upload section with Vue.js
tq-bit
tq-bit

Posted on • Updated on

Build a streaming drag and drop upload section with Vue.js

Fileuploads and the scope of this article

As this article's content is rather specific, please consider the following before reading ahead.

This article does show, how to:

✅ Directly deal with binary data in the browser, without the need for a dedicated input field.

✅ Put these into a format that can be streamed to a remote location with modern browser interfaces ( compatibility check at the end of the article ).

✅ Wrap the features up into a reusable Vue.js component. You can drop the resulting code into a .vue file and use it right away.

This article does not show, how to

❌ Extract the file from an - HTML tag inside a wrapping form - tag, which also includes the /post path

❌ Use a FormData object to which the file will be appended and sent to the server as a whole (even though that would also be doable)

Still on board? Then let's do this. Or jump right to the finished source code

Prerequisites

To follow along, you need to have a working version of Node.js and the Vue CLI installed on your machine, as well as a basic understanding of how Vue.js components work. The article was written using Vue 2.6.11 but it should work just as well with later versions

# Install the Vue CLI globally, in case you do not have it yet
$ npm i -g @vue/cli
Enter fullscreen mode Exit fullscreen mode

Get started

As the topic is very specific, let's start with cloning this Github template repository to your local machine. It includes a basic structure created with the Vue CLI. The most relevant file will be AppFileupload.vue inside the components folder.

Move into a dedicated project folder and execute the following commands:

# Clone the repository
$ git clone https://github.com/tq-bit/vue-upload-component.git
$ cd vue-upload-component

# Install node dependencies and run the development server
$ npm install
$ npm run serve
Enter fullscreen mode Exit fullscreen mode

Open up your browser at http://localhost:8080 to find this template app:

an image that shows the starting template of the drag and drop application

While you could use a standard file-input html tag to receive files per drag & drop, using other tags requires a bit of additional work. Let's look at the relevant html - template snippet:

<div class="upload-body">
 {{ bodyText || 'Drop your files here' }}
</div>
Enter fullscreen mode Exit fullscreen mode

To enable the desired functionality, we can use three browser event handlers and attach them to the upload-body. Each of them is fired by the browser as seen below:

Event Fires when
dragover The left mouse button is down and hovers over the element with a file
drop A file is dropped into the designated element's zone
dragleave The mouse leaves the element zone again without triggering the drop event

Vue's built-in vue-on directive makes it simple to attach functions to these events when bound to an element. Add the following directives to the template's upload-body tag:

<div 
 v-on:dragover.prevent="handleDragOver"
 v-on:drop.prevent="handleDrop"
 v-on:dragleave.prevent="handleDragLeave"
 class="upload-body"
 >
 {{ bodyText || 'Drop your files here' }}
</div>
Enter fullscreen mode Exit fullscreen mode

Also, within the data() - method in the - part, add these two indicators that change when above events are fired. We&#39;ll use them later for <a href="https://vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes">binding styles</a> and conditionally display our the footer.<br> <br>  </p> <div class="highlight"><pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span> <span class="nx">data</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="c1">// Create a property that holds the file information</span> <span class="na">file</span><span class="p">:</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">MyScreenshot.jpg</span><span class="dl">'</span><span class="p">,</span> <span class="na">size</span><span class="p">:</span> <span class="mi">281923</span><span class="p">,</span> <span class="p">},</span> <span class="c1">// Add the drag and drop status as an object</span> <span class="na">status</span><span class="p">:</span> <span class="p">{</span> <span class="na">over</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="na">dropped</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span> <span class="p">};</span> <span class="p">},</span> <span class="o">&lt;</span><span class="sr">/script</span><span class="err">&gt; </span></code></pre></div> <p></p> <p>Next, add the following three methods below. You could fill each of them with life to trigger other UI feedback, here, we&#39;ll focus on <code>handleDrop</code>.<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span> <span class="nx">data</span><span class="p">()</span> <span class="p">{...},</span> <span class="nx">methods</span><span class="p">:</span> <span class="p">{</span> <span class="nx">handleDragOver</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">over</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="p">},</span> <span class="nx">handleDrop</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">dropped</span> <span class="o">=</span> <span class="kc">true</span><span class="p">;</span> <span class="k">this</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">over</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">},</span> <span class="nx">handleDragLeave</span><span class="p">()</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">status</span><span class="p">.</span><span class="nx">over</span> <span class="o">=</span> <span class="kc">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="o">&lt;</span><span class="sr">/script</span><span class="err">&gt; </span></code></pre></div> <p></p> <p>Before we do, let us add two more directives to our html template to conditionally show some file metadata, and style the upload-body background.<br> </p> <div class="highlight"><pre class="highlight html"><code><span class="c">&lt;!-- The body will serve as our actual drag and drop zone --&gt;</span> <span class="nt">&lt;div</span> <span class="na">v-on:dragover.prevent=</span><span class="s">"handleDragOver"</span> <span class="na">v-on:drop.prevent=</span><span class="s">"handleDrop"</span> <span class="na">v-on:dragleave.prevent=</span><span class="s">"handleDragLeave"</span> <span class="na">class=</span><span class="s">"upload-body"</span> <span class="na">:class=</span><span class="s">"{'upload-body-dragged': status.over}"</span> <span class="nt">&gt;</span> {{ bodyText || 'Drop your files here' }} <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"upload-footer"</span><span class="nt">&gt;</span> <span class="nt">&lt;div</span> <span class="na">v-if=</span><span class="s">"status.dropped"</span><span class="nt">&gt;</span> <span class="c">&lt;!-- Display the information related to the file --&gt;</span> <span class="nt">&lt;p</span> <span class="na">class=</span><span class="s">"upload-footer-file-name"</span><span class="nt">&gt;</span>{{ file.name }}<span class="nt">&lt;/p&gt;</span> <span class="nt">&lt;small</span> <span class="na">class=</span><span class="s">"upload-footer-file-size"</span><span class="nt">&gt;</span>Size: {{ file.size }} kb<span class="nt">&lt;/small&gt;</span> <span class="nt">&lt;/div&gt;</span> <span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">"upload-footer-button"</span><span class="nt">&gt;</span> {{ footerText || 'Upload' }} <span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/div&gt;</span> </code></pre></div> <p></p> <p>Let&#39;s also add the necessary styles in the <style/> - section of the component to indicate when a file is hovering over the landing zone:<br> </p> <div class="highlight"><pre class="highlight css"><code><span class="o">&lt;</span><span class="nt">style</span><span class="o">&gt;</span> <span class="c">/* ... other classes*/</span> <span class="nc">.upload-body-dragged</span> <span class="p">{</span> <span class="nl">color</span><span class="p">:</span> <span class="m">#fff</span><span class="p">;</span> <span class="nl">background-color</span><span class="p">:</span> <span class="m">#b6d1ec</span><span class="p">;</span> <span class="p">}</span> <span class="o">&lt;/</span><span class="nt">style</span><span class="o">&gt;</span> </code></pre></div> <p></p> <p>Now try and throw a file inside - you&#39;ll notice the background turns blue while the footer text appears as the events are being fired.</p> <p><img src="https://q-bit.me/content/images/2021/01/02_Vue_upload_section_hover.jpg" alt=""></p> <p>So far so good. Let&#39;s now dive into the <code>handleDrop</code> method.</p> <h2> <a name="catch-the-dropped-file-and-process-it" href="#catch-the-dropped-file-and-process-it" class="anchor"> </a> Catch the dropped file and process it </h2> <p>The instant you drop the file, it becomes available as a property of the browser event. We can then call upon one of its methods to assign it to a variable.</p> <p>Add the following inside the <code>handleDrop()</code> method:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">fileItem</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">dataTransfer</span><span class="p">.</span><span class="nx">items</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">getAsFile</span><span class="p">();</span> </code></pre></div> <p></p> <p>This is what the browser&#39;s console displays the dropped item like. We do not only get access to the file itself, but also to a few useful information about it.</p> <p><img src="https://q-bit.me/content/images/2021/01/04_vue_screenshot_console.jpg" alt=""></p> <p>That&#39;s a perfect opportunity for some user feedback! Add the following to the bottom of the <code>handleDrop()</code> method:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="k">this</span><span class="p">.</span><span class="nx">file</span> <span class="o">=</span> <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="nx">fileItem</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span> <span class="na">size</span><span class="p">:</span> <span class="p">(</span><span class="nx">fileItem</span><span class="p">.</span><span class="nx">size</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">2</span><span class="p">),</span> <span class="p">};</span> </code></pre></div> <p></p> <p>Finally, we can now make use of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/FileReader">Filereader API</a> to catch the actual file contents and prepare it for further processing.  </p> <blockquote> <p>Please note that this process can be customized, based on your app&#39;s needs. The following procedure makes the file available as an <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer">ArrayBuffer</a> to process the image&#39;s raw binary data.</p> </blockquote> <p>Add the following to the bottom of the <code>handleDrop()</code> - method and optionally uncomment / remove unrelevant parts:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="kd">const</span> <span class="nx">reader</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FileReader</span><span class="p">();</span> <span class="c1">// Interchange these methods depending on your needs: </span> <span class="c1">// Read the file's content as text</span> <span class="c1">// reader.readAsText(fileItem);</span> <span class="c1">// Read the file's content as base64 encoded string, represented by a url</span> <span class="c1">// reader.readAsDataURL(fileItem);</span> <span class="c1">// Read the file's content as a raw binary data buffer</span> <span class="nx">reader</span><span class="p">.</span><span class="nx">readAsArrayBuffer</span><span class="p">(</span><span class="nx">fileItem</span><span class="p">);</span> <span class="c1">// Wait for the browser to finish reading and fire the onloaded-event:</span> <span class="nx">reader</span><span class="p">.</span><span class="nx">onloadend</span> <span class="o">=</span> <span class="nx">event</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Take the reader's result and use it for the next method</span> <span class="kd">const</span> <span class="nx">file</span> <span class="o">=</span> <span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">result</span><span class="p">;</span> <span class="k">this</span><span class="p">.</span><span class="nx">handleFileupload</span><span class="p">(</span><span class="nx">file</span><span class="p">);</span> <span class="c1">// Emit an event to the parent component</span> <span class="k">this</span><span class="p">.</span><span class="nx">$emit</span><span class="p">(</span><span class="dl">'</span><span class="s1">fileLoaded</span><span class="dl">'</span><span class="p">,</span> <span class="k">this</span><span class="p">.</span><span class="nx">file</span><span class="p">)</span> <span class="p">};</span> </code></pre></div> <p></p> <p>In a nutshell, an array buffer is the most generic type our file could take. While being performant, it might not always be the best choice. You can read more on the matter at <a href="https://javascript.info/arraybuffer-binary-arrays">javascript.info</a> and <a href="https://stackabuse.com/encoding-and-decoding-base64-strings-in-node-js/">this article on stackabuse</a>.</p> <h2> <a name="stream-the-file-to-a-server" href="#stream-the-file-to-a-server" class="anchor"> </a> Stream the file to a server </h2> <p>As stated, we will not send the file as a whole, but stream it to a receiving backend. Luckily, the browser&#39;s built in fetch API has this functionality by default.</p> <p>For the purpose to test our app, I&#39;ve created a <a href="https://vue-upload-server.herokuapp.com/">node.js service on heroku</a> that interprets whatever file is POSTed and sends back a basic response. You can find its source code here: <a href="https://github.com/tq-bit/vue-upload-server">https://github.com/tq-bit/vue-upload-server</a>.</p> <p>Let&#39;s use that one in our app. Add the following code as a method to your <code>AppFileupload.vue</code> file:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="k">async</span> <span class="nx">handleFileupload</span><span class="p">()</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">url</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">https://vue-upload-server.herokuapp.com/</span><span class="dl">'</span><span class="p">;</span> <span class="kd">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span> <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">post</span><span class="dl">'</span><span class="p">,</span> <span class="na">body</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">file</span><span class="p">.</span><span class="nx">value</span> <span class="p">};</span> <span class="k">try</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">response</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">fetch</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span> <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span> <span class="kd">const</span> <span class="p">{</span> <span class="nx">bytes</span><span class="p">,</span> <span class="nx">type</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">data</span><span class="p">;</span> <span class="nx">alert</span><span class="p">(</span><span class="s2">`Filesize: </span><span class="p">${(</span><span class="nx">bytes</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">).</span><span class="nx">toFixed</span><span class="p">(</span><span class="mi">2</span><span class="p">)}</span><span class="s2"> kb \nType: </span><span class="p">${</span><span class="nx">type</span><span class="p">.</span><span class="nx">mime</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span> <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">alert</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error! </span><span class="se">\n</span><span class="s1">An error occured: </span><span class="se">\n</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">e</span><span class="p">);</span> <span class="p">}</span> <span class="p">},</span> </code></pre></div> <p></p> <p>Now try to drop a file and hit &#39;Upload&#39; - if it goes well, you&#39;ll receive a response in the form of an alert with some basic info about your file.</p> <p><img src="https://q-bit.me/content/images/2021/01/06_vue_upload_alert.jpg" alt=""></p> <p>That&#39;s it. You&#39;ve got a fully functioning upload component. And you&#39;re not bound to vue.js. How about trying to integrate the same functionality into a vanilla project? Or extend the existing template and add custom properties for headingText and bodyText?</p> <p>To wrap this article up, you can find the finished Github repository below.<br><br> Happy coding</p> <p><a href="https://github.com/tq-bit/vue-upload-component/tree/done">https://github.com/tq-bit/vue-upload-component/tree/done</a></p> <h2> <a name="bonus-add-a-svg-loader" href="#bonus-add-a-svg-loader" class="anchor"> </a> Bonus: Add a svg loader </h2> <p>Since communication can take a moment, before wrapping up, let us add a loading indicator to our app. The svg I am using comes from <a href="https://loading.io/">loading.io</a>, a website that, besides paid loaders, also provides free svg loaders.</p> <p>In the <code>template</code> part of your component, replace the <code>upload-body</code> - div with the following:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="o">&lt;</span><span class="nx">div</span> <span class="nx">v</span><span class="o">-</span><span class="nx">on</span><span class="p">:</span><span class="nx">dragover</span><span class="p">.</span><span class="nx">prevent</span><span class="o">=</span><span class="dl">"</span><span class="s2">handleDragOver</span><span class="dl">"</span> <span class="nx">v</span><span class="o">-</span><span class="nx">on</span><span class="p">:</span><span class="nx">drop</span><span class="p">.</span><span class="nx">prevent</span><span class="o">=</span><span class="dl">"</span><span class="s2">handleDrop</span><span class="dl">"</span> <span class="nx">v</span><span class="o">-</span><span class="nx">on</span><span class="p">:</span><span class="nx">dragleave</span><span class="p">.</span><span class="nx">prevent</span><span class="o">=</span><span class="dl">"</span><span class="s2">handleDragLeave</span><span class="dl">"</span> <span class="kd">class</span><span class="o">=</span><span class="dl">"</span><span class="s2">upload-body</span><span class="dl">"</span> <span class="p">:</span><span class="kd">class</span><span class="o">=</span><span class="dl">"</span><span class="s2">{ 'upload-body-dragged': status.over }</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">svg</span> <span class="nx">v</span><span class="o">-</span><span class="k">if</span><span class="o">=</span><span class="dl">"</span><span class="s2">loading</span><span class="dl">"</span> <span class="nx">xmlns</span><span class="o">=</span><span class="dl">"</span><span class="s2">http://www.w3.org/2000/svg</span><span class="dl">"</span> <span class="nx">xmlns</span><span class="p">:</span><span class="nx">xlink</span><span class="o">=</span><span class="dl">"</span><span class="s2">http://www.w3.org/1999/xlink</span><span class="dl">"</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">margin: auto; display: block; shape-rendering: auto; animation-play-state: running; animation-delay: 0s;</span><span class="dl">"</span> <span class="nx">width</span><span class="o">=</span><span class="dl">"</span><span class="s2">160px</span><span class="dl">"</span> <span class="nx">height</span><span class="o">=</span><span class="dl">"</span><span class="s2">105px</span><span class="dl">"</span> <span class="nx">viewBox</span><span class="o">=</span><span class="dl">"</span><span class="s2">0 0 100 100</span><span class="dl">"</span> <span class="nx">preserveAspectRatio</span><span class="o">=</span><span class="dl">"</span><span class="s2">xMidYMid</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">path</span> <span class="nx">fill</span><span class="o">=</span><span class="dl">"</span><span class="s2">none</span><span class="dl">"</span> <span class="nx">stroke</span><span class="o">=</span><span class="dl">"</span><span class="s2">#486684</span><span class="dl">"</span> <span class="nx">stroke</span><span class="o">-</span><span class="nx">width</span><span class="o">=</span><span class="dl">"</span><span class="s2">8</span><span class="dl">"</span> <span class="nx">stroke</span><span class="o">-</span><span class="nx">dasharray</span><span class="o">=</span><span class="dl">"</span><span class="s2">42.76482137044271 42.76482137044271</span><span class="dl">"</span> <span class="nx">d</span><span class="o">=</span><span class="dl">"</span><span class="s2">M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z</span><span class="dl">"</span> <span class="nx">stroke</span><span class="o">-</span><span class="nx">linecap</span><span class="o">=</span><span class="dl">"</span><span class="s2">round</span><span class="dl">"</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">transform: scale(0.8); transform-origin: 50px 50px; animation-play-state: running; animation-delay: 0s;</span><span class="dl">"</span> <span class="o">&gt;</span> <span class="o">&lt;</span><span class="nx">animate</span> <span class="nx">attributeName</span><span class="o">=</span><span class="dl">"</span><span class="s2">stroke-dashoffset</span><span class="dl">"</span> <span class="nx">repeatCount</span><span class="o">=</span><span class="dl">"</span><span class="s2">indefinite</span><span class="dl">"</span> <span class="nx">dur</span><span class="o">=</span><span class="dl">"</span><span class="s2">1s</span><span class="dl">"</span> <span class="nx">keyTimes</span><span class="o">=</span><span class="dl">"</span><span class="s2">0;1</span><span class="dl">"</span> <span class="nx">values</span><span class="o">=</span><span class="dl">"</span><span class="s2">0;256.58892822265625</span><span class="dl">"</span> <span class="nx">style</span><span class="o">=</span><span class="dl">"</span><span class="s2">animation-play-state: running; animation-delay: 0s;</span><span class="dl">"</span> <span class="o">&gt;&lt;</span><span class="sr">/animate</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/path</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="sr">/svg</span><span class="err">&gt; </span> <span class="o">&lt;</span><span class="nx">span</span> <span class="nx">v</span><span class="o">-</span><span class="k">else</span><span class="o">&gt;</span><span class="p">{{</span> <span class="nx">bodyText</span> <span class="o">||</span> <span class="dl">'</span><span class="s1">Drop your files here</span><span class="dl">'</span> <span class="p">}}</span><span class="o">&lt;</span><span class="sr">/span</span><span class="err">&gt; </span><span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt; </span></code></pre></div> <p></p> <p>Also, add the following on top of your <code>data ()</code> - function:<br> </p> <div class="highlight"><pre class="highlight javascript"><code><span class="nx">data</span><span class="p">()</span> <span class="p">{</span> <span class="k">return</span> <span class="p">{</span> <span class="na">loading</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span> <span class="cm">/* ... other data props ... */</span> <span class="p">};</span> <span class="p">},</span> </code></pre></div> <p></p> <p>Now, when you upload a file, you should notice the loader to appear instead of the text.</p> <blockquote> <p>This post was originally published at <a href="https://blog.q-bit.me/build-a-drag-and-drop-upload-section-with-vue-js/">https://blog.q-bit.me/build-a-drag-and-drop-upload-section-with-vue-js/</a><br> Thank you for reading. If you enjoyed this article, let&#39;s stay in touch on Twitter 🐤 <a href="https://twitter.com/qbitme">@qbitme</a></p> </blockquote>

Top comments (0)