DEV Community

Cover image for Uploadnow - simple file explorer
Emmanuel Ajike
Emmanuel Ajike

Posted on

Uploadnow - simple file explorer

This is a submission for the The Pinata Challenge

What I Built

Uploadnow is a web application that allows users to upload and manage their files (images, videos, audios, etc.) easily. It utilizes Pinata for storage and retrieval, enabling a secure and consistent environment for asset management. This project is a submission for the Pinata Challenge. My inspiration is Uploadthing.

Prerequisites:

  • Basic knowledge of Node.js.
  • Pinata SDK for handling file uploads.
  • Convex.dev real-time database.
  • Next.js and Tailwind CSS.

Project setup

Run the following command to initialize a new ShadCN Next.js project:

npx shadcn@latest init
// or
pnpm dlx shadcn@latest init
Enter fullscreen mode Exit fullscreen mode

Create a .env.local file and copy the following into it:

PINATA_API_KEY=""
PINATA_API_SECRET=""
PINATA_GATEWAY="*.mypinata.cloud"
PINATA_JWT=""

# Deployment used by `npx convex dev`
CONVEX_DEPLOYMENT=""

NEXT_PUBLIC_CONVEX_URL=""

# Fingerprint config
FINGER_PRINT_API_KEY=""
Enter fullscreen mode Exit fullscreen mode

Features:

  1. Responsive design: The application is designed to be usable on mobile devices as well as desktop screens.
  2. Intuitive UI: The user interface is simplified, removing unnecessary UI elements and using easy-to-understand text instructions and toast notifications.
  3. Concise error messages: I focused on creating user-friendly error messages, thinking from a user's perspective, not just a developer's.
  4. Efficient file upload: The file upload feature allows users to upload various file types. Files are queued, and handlers like upload, cancel, retry, and remove file are provided.
  5. Multi-file uploads.
  6. Real-time support.
  7. Pagination.
  8. CDN usage: Users can open files in new tabs, and the links expire after a few seconds for added security.
  9. No login required: Browser fingerprinting is used to associate and differentiate users without requiring usernames and passwords.

Pinata Setup

This project requires credentials from Pinata, so head over to the dashboard.

sign-up page

  • Enter your details.

file-dashboard

  • This is your dashboard.

new-api-key

  • Click on the "API Keys" tab, then create a new key.

generate-key-with-permissions

  • Give your key a name, choose the appropriate permissions, and click "Generate".

generate-key

copy-credentials

  • Copy your credentials and store them securely because they are shown only once. Generate a new one if forgotten.

Demo

The deployed version of the application can be accessed at Uploadnow.

Loading state

Grid view

Upload queue

Upload progress

Upload done and toast

Context menu

My Code

1. Pinata upload

This is a Next.js route handler. It is a POST request handler that receives the file and other necessary details. The file is passed to the Pinata JavaScript SDK by calling the await pinata.upload.file(file) method, passing the file object as its first argument.

export const POST = async (req: Request) => {
  try {
    const form = await req.formData();

    const file = form.get("file") as File;
    const sidebarId = form.get("sideBarId") as Sidebar;
    const fingerPrintId = form.get("visitorId") as string;

    if (!file || !sidebarId || !fingerPrintId) {
      return Response.json(
        { message: "Invalid request, please provide arguments." },
        { status: 403 }
      );
    }

    const upload = await pinata.upload.file(file);

    const convexDb = await fetchMutation(api.files.createFile, {
      sidebarId,
      fingerPrintId,
      pinataId: upload.id,
      filename: upload.name,
      pinataCid: upload.cid,
      mimeType: upload.mime_type,
      pinataCreatedAt: upload.created_at,
    });

    if (!convexDb) {
      return Response.json(
        { message: "Was unable to upload file. Try again", uploaded: false },
        { status: 500 }
      );
    }

    return Response.json({ message: convexDb.message, uploaded: true });
  } catch (err: any) {
    console.error(err);

    return Response.json(
      { message: "Internal server error, please try again later." },
      { status: 500 }
    );
  }
};
Enter fullscreen mode Exit fullscreen mode

2. Setting up Convex DB Schema

Since this project uses Convex, it's important to set up the database structure. There is no need for a users table because we use Fingerprint.js.

import { defineSchema, defineTable, GenericSchema } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  files: defineTable({
    fingerPrintId: v.string(),
    pinataId: v.string(),
    pinataCid: v.string(),
    filename: v.string(),
    mimeType: v.string(),
    pinataCreatedAt: v.string(),
    sidebarId: v.union(
      v.literal("all"),
      v.literal("audios"),
      v.literal("videos"),
      v.literal("images"),
      v.literal("documents"),
      v.literal("archives")
    ),
  }),
});
Enter fullscreen mode Exit fullscreen mode

The complete source code for Uploadnow can be accessed via GitHub.

More Details

Gotchas:

  1. Buggy Group list method:

My initial intention was to integrate the groups feature of Pinata, but after the groups list method failed to work, I had to reconsider the feature.

const groups = await pinata.groups.list();
Enter fullscreen mode Exit fullscreen mode

When the method is called, it simply returns an empty array. I could not get it to work even after changing secret credentials. The REST API eventually worked, but by then I had already pivoted to other features. It's not a great developer experience juggling between REST API and SDK.

  1. Unable to send asset from the client:

The Pinata API is intuitive, but I could not find a way to send my assets from the client to Pinata directly. This feature would help save server resources by not uploading to my server first, then uploading to Pinata. I am not saying the current approach is bad, just that a client-side upload option would be better for developers, especially for features like pausable uploads.

Recommendations:

As a developer who values tools that enhance the developer experience, here are some recommendations:

  1. Provide default asset URLs: Allow developers to choose between generating signed URLs or using a default one. The lack of a default asset link can make developers reconsider integration strategies, which may push them toward alternative solutions. It also introduces UI challenges because developers need to be aware of the issue to effectively integrate the API.
  2. Return error and data tuples: The SDK should return both error and data tuples, letting developers choose how to handle each scenario.
// Example from Supabase
const { error, data } = await pinata.upload.file(file);

if (error) {
  throw new PlatformError("Pinata file upload failed.");
}
Enter fullscreen mode Exit fullscreen mode
  1. Add support for retry strategies: When the SDK fails, it should automatically retry, making it more resilient against errors.

I worked on this challenge alone.

Credits

I leveraged an open-source Shadcn template, which provided me with a base to build my app. Specifically, I used the upload dialog from uploader.sadmn.com to handle asset uploads. The rest of the application was built by me alone. The template contributed to less than 2% of the overall codebase.

Conclusion:

This challenge was very good, not too competitive or challenging, but very educational. I learned a lot about files and how to think about the user experience. That's all for today! Stay tuned for more updates and keep building amazing apps! πŸš€βœ¨
Happy coding! 😊

Top comments (0)