DEV Community

Cover image for Pixxsha - Share Your Moments Your Way
Ekemini Samuel
Ekemini Samuel

Posted on

Pixxsha - Share Your Moments Your Way

This is a submission for the The Pinata Challenge

Pixxsha - Share Your Moments Your Way

Pixxsha is a photo-sharing platform that lets you securely store and share your precious memories. Born from the DEV.to and Pinata challenge, Pixxsha aims to improve how we preserve and share our moments.

What Makes Pixxsha Special?

  • Secure Storage: Your photos are stored on IPFS, ensuring they're safe and always accessible.

  • Smart and Flexible Sharing: Share photos publicly, privately, or with a time limit - you're in control.
    /public: Share your photos with anyone using a simple link.
    /restricted: Control who gets access by adding specific people.
    /view Once: Send a link that disappears after it’s been viewed.

  • Organized Memories: Create custom groups for your photos, like "My 21st Birthday" or "Baby's First Year".

Sign in to Pixxsha 😎
pixxsha

The Future of Pixxsha

We're just getting started! Here are some exciting features we're working on:

🌟 Life Timeline: Create a visual journey of your life with photo groups for significant milestones.

👨‍👩‍👧‍👦 Family Albums: Dedicated spaces for family memories, like "Baby's First Steps" or "Family Vacations".

🤝 Collaborative Groups: Share and contribute to group albums with friends and family.


Thank You, DEV Community and Pinata!

This challenge sparked the idea for Pixxsha, and we're excited to see where it goes. We're grateful for the opportunity to build something that could change how people cherish their memories.

Demo

Pixxsha's dashboard

dashboard

Our Code

The GitHub repository for Pixxsha

More Details of how we integrated Pinata

Pinata and IPFS

We've integrated Pinata's Web3 SDK to handle our IPFS interactions. Here's a glimpse of how we're using it:

  • From the pinataService.ts
import { PinataSDK } from 'pinata-web3';

const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,
  pinataGateway: process.env.PINATA_GATEWAY!,
});

// Upload a file to Pinata with optional tags and group assignment
export const uploadFile = async (file: File, tags: Record<string, string>, groupId?: string) => {
  try {
    const upload = await pinata.upload.file(file, {
      metadata: {
        name: file.name,
        keyvalues: tags,
      },
    });

    if (groupId) {
      await pinata.groups.addCids({
        groupId,
        cids: [upload.IpfsHash],
      });
    }

    return upload;
  } catch (error) {
    throw new Error(`Failed to upload file: ${error.message}`);
  }
};

// Retrieve a file from Pinata using its IPFS hash
export const retrieveFile = async (ipfsHash: string) => {
  try {
    const data = await pinata.gateways.get(ipfsHash);
    return data;
  } catch (error) {
    throw new Error(`Failed to retrieve file: ${error.message}`);
  }
};

// Create a new group in Pinata
export const createGroup = async (name: string) => {
  try {
    return await pinata.groups.create({ name });
  } catch (error) {
    throw new Error(`Failed to create group: ${error.message}`);
  }
};

// Get details of a specific group by ID
export const getGroup = async (groupId: string) => {
  try {
    return await pinata.groups.get({ groupId });
  } catch (error) {
    throw new Error(`Failed to get group: ${error.message}`);
  }
};

// List all groups with optional filtering by name, offset, and limit
export const listGroups = async (name?: string, offset?: number, limit?: number) => {
  try {
    const query = pinata.groups.list();
    if (name) query.name(name);
    if (offset) query.offset(offset);
    if (limit) query.limit(limit);
    return await query;
  } catch (error) {
    throw new Error(`Failed to list groups: ${error.message}`);
  }
};

// Update the name of a group
export const updateGroup = async (groupId: string, name: string) => {
  try {
    return await pinata.groups.update({ groupId, name });
  } catch (error) {
    throw new Error(`Failed to update group: ${error.message}`);
  }
};

// Delete a group by ID
export const deleteGroup = async (groupId: string) => {
  try {
    await pinata.groups.delete({ groupId });
  } catch (error) {
    throw new Error(`Failed to delete group: ${error.message}`);
  }
};

// Add CIDs to a group
export const addCidsToGroup = async (groupId: string, cids: string[]) => {
  try {
    await pinata.groups.addCids({ groupId, cids });
  } catch (error) {
    throw new Error(`Failed to add CIDs to group: ${error.message}`);
  }
};

// Remove CIDs from a group
export const removeCidsFromGroup = async (groupId: string, cids: string[]) => {
  try {
    await pinata.groups.removeCids({ groupId, cids });
  } catch (error) {
    throw new Error(`Failed to remove CIDs from group: ${error.message}`);
  }
};
;

Enter fullscreen mode Exit fullscreen mode
  • In our photoController.ts, here's how we're handling photo sharing:
export const sharePhoto = async (req: Request, res: Response) => {
    const { photoId, shareType, recipientEmails } = req.body;

    try {
      const { data: photo, error } = await supabase
        .from('photos')
        .select('*')
        .eq('id', photoId)
        .single();

      if (error) throw error;

      if (photo.user_id !== (req as any).user.userId) {
        return res.status(403).json({ error: 'Not authorized to share this photo' });
      }

      const shareLink = `${config.pinataGateway}/ipfs/${photo.ipfs_hash}`;

      let expirationDate = null;
      let viewCount = null;

      switch (shareType) {
        case 'public':
          break;
        case 'restricted':
          if (!recipientEmails || recipientEmails.length === 0) {
            return res.status(400).json({ error: 'Recipient emails are required for restricted sharing' });
          }
          break;
        case 'view-once':
          viewCount = 1;
          break;
        case 'time-limited':
          expirationDate = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours from now
          break;
        default:
          return res.status(400).json({ error: 'Invalid share type' });
      }

      const { data: share, error: shareError } = await supabase
        .from('shares')
        .insert({
          photo_id: photoId,
          share_type: shareType,
          share_link: shareLink,
          recipient_emails: recipientEmails,
          expiration_date: expirationDate,
          view_count: viewCount,
          remaining_views: viewCount,
        });

      if (shareError) throw shareError;

      res.json({ shareLink, share });
    } catch (error: any) {
      res.status(500).json({ error: error.message });
    }
  };

Enter fullscreen mode Exit fullscreen mode
  • Organizing Photos with Pinata Groups
export const addCidsToGroup = async (groupId: string, cids: string[]) => {
  return await pinata.groups.addCids({ groupId, cids });
};

Enter fullscreen mode Exit fullscreen mode

This allows users to organize their photos into custom collections like "Vacation 2023" or "Family Portraits".

  • Dashboard Integration We created a dashboard that leverages Pinata's group and file management:
export const getDashboardData = async (req: Request, res: Response) => {
  // Fetch photos with their tags and groups
  const { data: photos } = await supabase
    .from('photos')
    .select(`*, tags (name), groups (name)`)
    .eq('user_id', userId);

  // ... aggregate data for dashboard
  res.json({ photos, photoCount, recentUploads });
};

Enter fullscreen mode Exit fullscreen mode

What's Next?

We're continuing to develop Pixxsha, focusing on enhancing user experience and adding more features. Stay tuned for updates, and feel free to reach out if you'd like to be part of our journey!


Team members

Top comments (2)

Collapse
 
gregharis profile image
Grëg Häris

Amazing

Collapse
 
envitab profile image
Ekemini Samuel

Thank you!