Skip to content
Merged
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
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
**DeCleanup Rewards** is a decentralized application (dApp) build on steller network and designed to:
a) **tokenize IRL impact created via global/local cleanup efforts**
b) **incentivize environmental actions via DeFi mechanics**.
b) **incentivize environmental actions via DeFi mechanics**.
The platform promotes environmental stewardship globally, with plans to activate global/local communities and scale across multiple blockchain ecosystems.

![Screenshot of the Main Page](https://beige-defiant-spoonbill-537.mypinata.cloud/ipfs/QmWjckBnwWkidWtTQwR17TrQWoo9j3FX5LLwRg8s3n12cN)
Expand All @@ -11,13 +11,13 @@ The platform promotes environmental stewardship globally, with plans to activate

**Smart Contract GitHub Repository** - 🔗👉 https://github.com/DeCleanUp-DCU/Smart-Contract

**Smart Contract onchain** [Arbiscan](https://arbiscan.io/address/0xf21389b64e0eb749fd150d0c44742692e19a69c8)
**Smart Contract onchain** [Arbiscan](https://arbiscan.io/address/0xf21389b64e0eb749fd150d0c44742692e19a69c8)

## Uniqueness

**How DeCleanup dApp works**: Participants engage in cleanups, submit proof via photos, and earn dynamic NFTs that evolve based on their contributions. More details in our [Mirror Article](https://mirror.xyz/decleanupnet.eth/ZzncKRu-Q-leEZkQ48Txm-NQRxG_hH3V3wyHaYUKfYI).

**User-Centric Design & Functionality**: The DeCleanup dApp offers a seamless user experience with an intuitive interface, allowing individuals and communities to easily submit proof of cleanups and earn dynamic NFTs, reflecting real-world impact on-chain. The verification system ensures quick approval for rewards, making the process simple and efficient.
**User-Centric Design & Functionality**: The DeCleanup dApp offers a seamless user experience with an intuitive interface, allowing individuals and communities to easily submit proof of cleanups and earn dynamic NFTs, reflecting real-world impact on-chain. The verification system ensures quick approval for rewards, making the process simple and efficient.
More details in our [Guide](https://mirror.xyz/decleanupnet.eth/A5uzOpx9HUgXZEopCKUsbgffw9SajL4Q9eECgrguq-4).

**Gasless Approach**: We chose Arbitrum for its low gas fees, with only the first claim requiring gas payment, while the rest of the experience is gasless. This eliminates barriers to onboarding non-tech-savvy users into Web3, enhancing real-world impact - our key KPI. Gasless transactions make Web3 more accessible to everyday people.
Expand All @@ -33,19 +33,22 @@ More details in our [Guide](https://mirror.xyz/decleanupnet.eth/A5uzOpx9HUgXZEop
![9 Levels NFTs](https://beige-defiant-spoonbill-537.mypinata.cloud/ipfs/QmZELVjF8H5VvG1BxhunXK4n6LuK17RBuis5yRepEqxARk)

The collection items have common traits that remain consistent with each NFT upgrade:

- **Category**: Cleanup Impact NFT
- **Type**: Dynamic
- **Impact**: Environmental

There are dynamic traits, which grow every three cleanups:

- **Rarity**: Common (1-3 cleanups), Rare (4-6 cleanups), Legendary (7-9 cleanups), Unique (10 cleanups)
- **Level**: Newbie (1-3 cleanups), Pro (4-6 cleanups), Hero (7-9 cleanups), Guardian (10 cleanups)

And dynamic traits which grow with every cleanup done:

- **Impact Value**: 1 (first cleanup) to 10 (last cleanup)
- **DCU Points**: 10 (first cleanup) to 100 (last cleanup)

While the first two categories of traits determine the general information required for this impact product’s classification on the Impact Marketplace, the last categories are mainly dynamic traits, which will determine the rewards value via Impact Marketplace DeFi utility and will contribute for user’s IRL impact rank.
While the first two categories of traits determine the general information required for this impact product’s classification on the Impact Marketplace, the last categories are mainly dynamic traits, which will determine the rewards value via Impact Marketplace DeFi utility and will contribute for user’s IRL impact rank.
Read more on this topic in the article by EcoSynthesisX on [Mirror](https://mirror.xyz/ecosynthesisx.eth/zOdeuaeFfJUFScZZKu1OGF7cWCiRgUHQSGE-14cf8fo).

![Example of Traits](https://beige-defiant-spoonbill-537.mypinata.cloud/ipfs/QmfUA1PomqfsXPZod2oo79nrMq17xT1Rxo8EdWxwFFVHxM)
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"react-medium-image-zoom": "^5.2.12",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "^5.0.3",
"thirdweb": "^5.0.3",
"viem": "^2.26.2",
"wagmi": "^2.14.16"
},
Expand All @@ -54,10 +54,10 @@
"@types/react": "^19.0.1",
"@types/react-dom": "^19",
"@types/react-window": "^1.8.8",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"autoprefixer": "^10.4.20",
"eslint": "8.57.1",
"eslint": "8.57.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-next": "^15.1.0",
"eslint-config-prettier": "^9.1.0",
Expand All @@ -76,4 +76,4 @@
"tailwindcss": "^3.4.16",
"typescript": "^5.7.2"
}
}
}
40 changes: 22 additions & 18 deletions src/app/api/auth/[...nextauth].ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,31 @@
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'

// Extend the Session and User types to include the idToken
declare module "next-auth" {
declare module 'next-auth' {
interface User {
idToken?: string;
idToken?: string
}
interface Session {
idToken?: string | undefined;
idToken?: string | undefined
}
}

export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? (() => { throw new Error("GOOGLE_CLIENT_SECRET is not defined"); })(),
clientId: process.env.GOOGLE_CLIENT_ID ?? '',
clientSecret:
process.env.GOOGLE_CLIENT_SECRET ??
(() => {
throw new Error('GOOGLE_CLIENT_SECRET is not defined')
})(),
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
scope: "openid profile email",
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
scope: 'openid profile email',
},
},
}),
Expand All @@ -30,19 +34,19 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
// Store the Google id_token in the JWT
async jwt({ token, account }) {
if (account) {
token.idToken = account.id_token;
token.idToken = account.id_token
}
return token;
return token
},
// Add the id_token to the session
async session({ session, token }) {
session.idToken = token.idToken as string;
return session;
session.idToken = token.idToken as string
return session
},
},
session: {
strategy: "jwt",
strategy: 'jwt',
},
});
})

export default handlers;
export default handlers
72 changes: 41 additions & 31 deletions src/app/api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,81 @@
"use client"
import type { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";
import { createThirdwebClient, generatePayload, getUser, verifyPayload } from "thirdweb";
import { inMemoryWallet } from "thirdweb/wallets";
import { auth } from "./[...nextauth]";
import { serialize } from "cookie";
'use client'
import type { NextApiRequest, NextApiResponse } from 'next'
import { getServerSession } from 'next-auth'
import {
createThirdwebClient,
generatePayload,
getUser,
verifyPayload,
} from 'thirdweb'
import { inMemoryWallet } from 'thirdweb/wallets'
import { auth } from './[...nextauth]'
import { serialize } from 'cookie'

// Initialize ThirdWeb client
const client = createThirdwebClient({
clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID,
});
})

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
return res.status(405).json({ error: "Method not allowed" });
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' })
}

try {
// Get the NextAuth session (Google OAuth)
const session = await getServerSession(req, res, auth);
const session = await getServerSession(req, res, auth)
if (!session || !session.idToken) {
return res.status(401).json({ error: "Unauthorized" });
return res.status(401).json({ error: 'Unauthorized' })
}

// Use the Google id_token as the identifier
const idToken = session.idToken;
const idToken = session.idToken

// Generate a ThirdWeb Auth payload
const payload = await generatePayload({
client,
address: idToken, // Use id_token as the unique identifier
});
})

// Sign the payload with an in-memory wallet (for demo purposes)
const wallet = inMemoryWallet();
const signedPayload = await wallet.signPayload(payload);
const wallet = inMemoryWallet()
const signedPayload = await wallet.signPayload(payload)

// Verify the signed payload
const verified = await verifyPayload({
client,
payload: signedPayload,
});
})

if (!verified) {
return res.status(401).json({ error: "Verification failed" });
return res.status(401).json({ error: 'Verification failed' })
}

// Get the ThirdWeb user (includes wallet address)
const thirdwebUser = await getUser({ client, address: idToken });
const thirdwebUser = await getUser({ client, address: idToken })

// Store the ThirdWeb session in a cookie
const sessionData = {
address: thirdwebUser.address,
idToken,
};
const encryptedSessionData = JSON.stringify(sessionData); // In production, encrypt this
const cookie = serialize("thirdweb_session", encryptedSessionData, {
}
const encryptedSessionData = JSON.stringify(sessionData) // In production, encrypt this
const cookie = serialize('thirdweb_session', encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 1 week
path: "/",
});
path: '/',
})

res.setHeader("Set-Cookie", cookie);
res.status(200).json({ message: "Login successful", address: thirdwebUser.address });
res.setHeader('Set-Cookie', cookie)
res
.status(200)
.json({ message: 'Login successful', address: thirdwebUser.address })
} catch (error) {
console.error("Login error:", error);
res.status(500).json({ error: "Internal server error" });
console.error('Login error:', error)
res.status(500).json({ error: 'Internal server error' })
}
}
}
8 changes: 5 additions & 3 deletions src/app/client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { createThirdwebClient } from "thirdweb";
import { createThirdwebClient } from 'thirdweb'



const clientId = process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID;


if (!clientId) {
throw new Error("No client ID provided");
throw new Error('No client ID provided')
}

export const client = createThirdwebClient({
clientId: clientId,
});
})
14 changes: 7 additions & 7 deletions src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,12 @@ export default function Page() {
console.log('Uploaded images:', images)
}
return (
<div className='h-[calc(100vh-160px)] min-h-[calc(98vh-160px)] md:min-h-[calc(94vh-160px)] bg-[#58B12F] font-bebas font-bold my-4 lg:p-4'>
<div className='my-4 h-[calc(100vh-160px)] min-h-[calc(98vh-160px)] bg-[#58B12F] font-bebas font-bold md:min-h-[calc(94vh-160px)] lg:p-4'>
<div className='flex flex-col items-start justify-between lg:flex-row lg:gap-12'>


<div className='mt-4 flex w-full flex-col bg-[#FAFF00] lg:order-first lg:w-1/3 lg:bg-[#58B12F]'>
{/* 24 WEEKS STREAK*/}
<div className='flex h-[61px] w-full items-center justify-between py-8'>
<div className='mx-3 mt-3 md:mx-auto w-full border-4 border-black'>
<div className='mx-3 mt-3 w-full border-4 border-black md:mx-auto'>
<div className='mx-auto flex w-fit lg:w-full'>
<div className='hidden items-center justify-center bg-black p-5 lg:flex'>
<svg
Expand Down Expand Up @@ -82,7 +80,9 @@ export default function Page() {
<div className='mt-6 flex flex-col space-y-1 px-4'>
<div className='flex items-center justify-between border-b border-b-black'>
<p className='md:text-[24px]'>CLEANUPS DONE</p>
<p className='md:text-[24px] text-[#58B12F] md:text-[#FAFF00]'>0</p>
<p className='text-[#58B12F] md:text-[24px] md:text-[#FAFF00]'>
0
</p>
</div>
<div className='flex items-center justify-between border-b border-b-black'>
<p className='md:text-[24px]'>REFERRALS </p>
Expand Down Expand Up @@ -166,7 +166,7 @@ export default function Page() {
</div>
</div>

<div className='flex w-full flex-col space-y-2 px-4 lg:w-1/3'>
<div className='flex w-full flex-col space-y-2 px-4 lg:w-1/3'>
<LongButton text='CREATE IMPACT CIRCLE' isNotBlack />
<LongButton text='JOIN IMPACT CIRCLE' isNotBlack />
<LongButton text='BECOME VERIFIER' isNotBlack />
Expand Down Expand Up @@ -202,7 +202,7 @@ function LongButton({ text, isNotBlack }: LongButtonProps) {
<button
className={
isNotBlack
? `block md:h-[60px] w-full bg-[#1E8428] py-2 text-center text-2xl md:text-3xl text-black opacity-50 md:text-[40px]`
? `block w-full bg-[#1E8428] py-2 text-center text-2xl text-black opacity-50 md:h-[60px] md:text-3xl md:text-[40px]`
: `block h-[60px] w-full bg-black text-center text-3xl text-[#FAFF00] md:text-[40px]`
}
>
Expand Down
Loading