Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pin Save - decentralized Pinterest
# PinSave - decentralized decentalized image sharing platform

<p align="center">
<img src="https://raw.githubusercontent.com/Pfed-prog/Dspyt-NFTs-EVM/master/packages/frontend/public/PinSaveL.png" alt="Size Limit CLI" width="738" >
Expand All @@ -18,7 +18,7 @@

</div>

Pin Save is a decentralized image, video sharing and content aggregation platform where community controls the platform.
PinSave is a decentralized image and content aggregation platform where community controls the platform.

1. The decentralized feed reinforces the discovery of content and feedback.
2. Upgradeable, resilient decentralized storage.
Expand All @@ -32,15 +32,15 @@ Pin Save is a decentralized image, video sharing and content aggregation platfor

- Image posting:

![Pin Save Upload](https://raw.githubusercontent.com/PinSaveDAO/PinSave-Metis/refs/heads/main/assets/upload.png)
![PinSave Upload](https://raw.githubusercontent.com/PinSaveDAO/PinSave-Metis/refs/heads/main/assets/upload.png)

- Profile Page with ENS:

![Pin Save Upload](https://raw.githubusercontent.com/PinSaveDAO/PinSave-Metis/refs/heads/main/assets/profile.png)
![PinSave Upload](https://raw.githubusercontent.com/PinSaveDAO/PinSave-Metis/refs/heads/main/assets/profile.png)

### Optimism Smart contracts
### Metis PinSave Smart contracts

[Metis Smart contract Etherscan](https://explorer.metis.io/token/0x6F67850013b5775E36E35071a5CdD16ea43e1061)
[Metis PinSave Smart contract Etherscan](https://explorer.metis.io/token/0x6F67850013b5775E36E35071a5CdD16ea43e1061)

## Setup

Expand All @@ -54,9 +54,8 @@ yarn dev
## Further Resources

- [PinSave Figma Resources](https://www.figma.com/community/file/1102944149244783025)
- [Zk Ok Pin Save](https://zkok.io/mina/pin-save/)
- [EthBucharest 2024: Zero Knowledge proofs on Mina, zkPassport and SoulBound NFTs](https://docs.google.com/presentation/d/1OmJJgzk4iFbKexqBw87oU7oh4H9lXlFFh3eas0EF9y8/edit?usp=sharing)
- [EthBucharest 2024 PinSave: Zero Knowledge proofs on Mina, zkPassport and SoulBound NFTs](https://docs.google.com/presentation/d/1OmJJgzk4iFbKexqBw87oU7oh4H9lXlFFh3eas0EF9y8/edit?usp=sharing)
- [PinSave.app DR](https://ahrefs.com/website-authority-checker/?input=pinsave.app)
- [Npm Pin Save mina package](https://www.npmjs.com/package/pin-mina)
- [Pin Save on Dspyt](https://dspyt.com/PinSave)
- [Pin Save retroPGF3](https://round3.optimism.io/projects/0xc613e2a991ce0dbcf8fae1d6128e67543da9710e14831112fba654cc8fe8c389)
- [Npm PinSave mina package](https://www.npmjs.com/package/pin-mina)
- [PinSave on Dspyt](https://dspyt.com/PinSave)
- [PinSave retroPGF3](https://round3.optimism.io/projects/0xc613e2a991ce0dbcf8fae1d6128e67543da9710e14831112fba654cc8fe8c389)
2 changes: 1 addition & 1 deletion packages/frontend/LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MIT License

Copyright (c) Pavel Fedotov 2024
Copyright (c) Pavel Fedotov 2025

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/components/Post/DisplayMedia.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const DisplayMedia: React.FC<IMyProps> = ({ post }) => {
width={width}
src={post.image}
alt={post.name}
loading="lazy"
style={{
height: "95%",
borderRadius: "10px",
Expand Down
8 changes: 6 additions & 2 deletions packages/frontend/components/Posts/PostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { Paper, Text } from "@mantine/core";
import Image from "next/image";
import Link from "next/link";

import type { Post } from "@/services/upload";
type PostReduced = {
image: string;
name: string;
tokenId: number;
};

interface IMyProps {
post: Post;
post: PostReduced;
}

const PostCard: React.FC<IMyProps> = ({ post }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/frontend/components/SEO/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const CommonSEO = ({
content={`${siteMetadata.siteUrl}${router.asPath}`}
/>
<meta property="description" content={description} />
<link rel="canonical" href={`${siteMetadata.siteUrl}${router.asPath}`} />
<meta property="og:description" content={description} />
<meta property="og:type" content={siteMetadata.ogType} />
<meta property="og:image" content={ogImage} />
Expand Down
6 changes: 3 additions & 3 deletions packages/frontend/components/SEO/siteMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export const siteMetadata = {
title: "Pin Save - decentralized Pinterest",
title: "PinSave - Decentralized Image Sharing & Content Aggregation Platform",
author: "Pavel Fedotov",
headerTitle: "Pin Save - decentralized Pinterest",
headerTitle: "PinSave - Decentralized Image Sharing",
description:
"Pin Save is a platform for decentralized content aggregation and image sharing where users have content ownership.",
"PinSave is a platform for decentralized content aggregation and image sharing where users have content ownership.",
ogType: "website",
ogImageUrl: "https://metis.pinsave.app/TwitterIconWords.png",
siteUrl: "https://metis.pinsave.app",
Expand Down
127 changes: 71 additions & 56 deletions packages/frontend/components/UploadForm/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
Paper,
Title,
TextInput,
Textarea,
Group,
Button,
Image,
Expand All @@ -13,7 +12,6 @@ import {
} from "@mantine/core";
import { Dropzone, MIME_TYPES } from "@mantine/dropzone";
import { useState, useEffect } from "react";
import ReactPlayer from "react-player";
import { Upload, Replace } from "tabler-icons-react";
import {
useAccount,
Expand All @@ -29,24 +27,21 @@ import { getContractInfo } from "@/utils/contracts";

export const dropzoneChildren = (image: File | undefined) => {
if (image) {
let link = URL.createObjectURL(image);
let link: string = URL.createObjectURL(image);
return (
<Group
position="center"
spacing="xl"
style={{ minHeight: 220, pointerEvents: "none" }}
>
{image.type[0] === "i" ? (
<Image
src={link}
alt="uploaded image"
my="md"
radius="lg"
sx={{ maxWidth: "240px" }}
/>
) : (
<ReactPlayer url={link} />
)}
<Image
src={link}
alt="uploaded image"
my="md"
radius="lg"
sx={{ maxWidth: "240px" }}
/>

<Group sx={{ color: "#3a3a3a79" }}>
<MediaQuery
query="(max-width:500px)"
Expand Down Expand Up @@ -90,30 +85,31 @@ const UploadForm = () => {
const { address: senderAddress } = useAccount();

const { address: contractAddress, abi } = getContractInfo();
const { data: hash, writeContract: writeMintPost } = useWriteContract();
const {
data: hash,
writeContract: writeMintPost,
status,
} = useWriteContract();

const [name, setName] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [image, setImage] = useState<File | undefined>();
const [postReceiver, setPostReceiver] = useState<`0x${string}` | undefined>(
senderAddress
);

const [isPostUpdated, setIsPostUpdated] = useState<boolean>(false);
const [isPostLoading, setIsPostLoading] = useState<boolean>(false);

const [cid, setCid] = useState<string>("");

const [ensName, setEnsName] = useState<string>("");
const [provider, setProvider] = useState<"Pinata">("Pinata");

const [lastHash, setLastHash] = useState<string>("");
const [isPostReady, setIsPostReady] = useState<boolean>(false);
const [isPostLoading, setIsPostLoading] = useState<boolean>(false);

const [ensName, setEnsName] = useState<string>("");
const [cid, setCID] = useState<string>("");
const [lastHash, setLastHash] = useState<string | undefined>();

const config = createConfig({
chains: [mainnet],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_ALCHEMY),
[mainnet.id]: http(process.env.NEXT_PUBLIC_ALCHEMY_MAINNET),
},
});

Expand All @@ -129,6 +125,7 @@ const UploadForm = () => {
image?: File
) {
if (description !== "" && name !== "" && image && postReceiver) {
setIsPostLoading(true);
const cid: string | undefined = await UploadData({
data: { name: name, description: description, image: image },
provider: provider,
Expand All @@ -138,13 +135,12 @@ const UploadForm = () => {
throw new Error("no cid");
}

setCid(cid);
setCID(cid);
setIsPostReady(true);

setImage(undefined);
setName("");
setDescription("");

setIsPostLoading(true);
}
}

Expand All @@ -160,14 +156,9 @@ const UploadForm = () => {
setPostReceiver(receiverAddress);
}

if (isPostLoading) {
setIsPostUpdated(true);
console.log("Updated response:" + cid);
}

if (hash && hash !== lastHash && isPostUpdated) {
if (hash && hash !== lastHash && isPostReady) {
setLastHash(hash);
setIsPostUpdated(false);
setIsPostReady(false);
setIsPostLoading(false);
}
}, [
Expand All @@ -176,10 +167,11 @@ const UploadForm = () => {
receiverAddress,
lastHash,
cid,
isPostUpdated,
isPostReady,
senderAddress,
ensName,
hash,
fetchedAccount,
]);

return (
Expand Down Expand Up @@ -214,7 +206,7 @@ const UploadForm = () => {
</Title>
)}

{!isPostUpdated ? (
{!isPostLoading ? (
<div>
<TextInput
required
Expand All @@ -223,7 +215,7 @@ const UploadForm = () => {
value={name}
onChange={(e) => setName(e.target.value)}
/>
<Textarea
<TextInput
my="md"
required
onChange={(e) => setDescription(e.target.value)}
Expand Down Expand Up @@ -261,13 +253,12 @@ const UploadForm = () => {
MIME_TYPES.webp,
MIME_TYPES.svg,
MIME_TYPES.gif,
MIME_TYPES.mp4,
]}
>
{() => dropzoneChildren(image)}
</Dropzone>
<Group position="center" sx={{ padding: 10 }}>
{senderAddress && (
{senderAddress && !isPostLoading && (
<Button
component="a"
radius="lg"
Expand All @@ -280,6 +271,17 @@ const UploadForm = () => {
</Button>
)}
</Group>
{lastHash && (
<>
<Center>
<Text>{status}</Text>
</Center>
<Center>
<Text>{lastHash}</Text>
</Center>
</>
)}

<Center>
<NativeSelect
placeholder="Pick IPFS Provider"
Expand All @@ -293,23 +295,36 @@ const UploadForm = () => {
</Center>
</div>
) : (
<Center>
<Button
component="a"
radius="lg"
mt="md"
onClick={() => {
writeMintPost({
address: contractAddress,
abi: abi,
functionName: "createPost",
args: [postReceiver, cid],
});
}}
>
Upload Post
</Button>
</Center>
<>
{isPostReady ? (
<>
<Center>
<Button
component="a"
radius="lg"
mt="md"
onClick={() => {
writeMintPost({
address: contractAddress,
abi: abi,
functionName: "createPost",
args: [postReceiver, cid],
});
}}
>
Upload Post
</Button>
</Center>
<Center mt="md">
<Text>CID: {cid}</Text>
</Center>
</>
) : (
<Center>
<Text>Uploading to IPFS</Text>
</Center>
)}
</>
)}
</Paper>
</div>
Expand Down
4 changes: 3 additions & 1 deletion packages/frontend/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ declare global {
NEXT_PUBLIC_PINATA_JWT: string;
NEXT_PUBLIC_GATEWAY_URL: string;
NEXT_PUBLIC_WALLETCONNECT_ID: string;
NEXT_PUBLIC_ALCHEMY: string;
NEXT_PUBLIC_ALCHEMY_MAINNET: string;
NEXT_PUBLIC_ALCHEMY_METIS_FIRST: string;
NEXT_PUBLIC_ALCHEMY_METIS_SECOND: string;
ENV: "test" | "dev" | "prod";
}
}
Expand Down
5 changes: 3 additions & 2 deletions packages/frontend/hooks/api/posts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ export const usePosts = () => {
},
initialPageParam: undefined,
getNextPageParam: (lastPage: any, pages: any) => {
if (lastPage.items[5]?.token_id < lastPage.totalSupply) {
if (lastPage.items[5]?.tokenId < lastPage.totalSupply) {
return pages.length + 1;
}
},
});
};

export const usePost = (id: string) => {
export const usePost = (id: number, enabled: boolean) => {
return useQuery({
queryKey: [id],
queryFn: () => fetchPost(id),
enabled: enabled,
});
};
2 changes: 1 addition & 1 deletion packages/frontend/hooks/api/posts/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const fetchPosts = async ({
}
};

export const fetchPost = async (id: string) => {
export const fetchPost = async (id: number) => {
try {
return await fetcher(`/api/metis/posts/${id}`);
} catch (error) {
Expand Down
Loading