diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 5d3e3b6..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -.next/ -.env -.env.local -.env.*.local -**/dependencies/ diff --git a/.gitmodules b/.gitmodules index 888d42d..d4f5e45 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "lib/forge-std"] - path = lib/forge-std +[submodule "vlayer/lib/forge-std"] + path = vlayer/lib/forge-std url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md index 9265b45..6badc1e 100644 --- a/README.md +++ b/README.md @@ -1,66 +1,19 @@ -## Foundry +# Compass 🧭 -**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** +![🧭 Compass](https://github.com/user-attachments/assets/4edaeabb-2a20-4ad7-a8dc-2ee4b97b286e) -Foundry consists of: +_Compass connects verified (g)local issues with builders and bounties at global hackathons._ -- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). -- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. -- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. -- **Chisel**: Fast, utilitarian, and verbose solidity REPL. +Compass is a platform designed to reorient global hackathons toward grounded, collective impact by connecting local community stakeholders and their needs with builders, funders, and organizers. Through a system of verified issue-sourcing, Compass enables communities —especially those historically excluded from innovation spaces— to articulate concrete problems they face. These needs are translated into actionable prompts and integrated into the hackathon's unique workflow as complements to bounties, featured challenges, or curated tracks, compatible with existing parameters on the organizer's side, and as hackathon ideas on the builder's side. -## Documentation +Rather than relying on the benevolence of sponsors or organizers to champion local relevance, Compass embeds mechanisms of accountability, visibility, and reciprocity at the protocol and UI/UX levels. It makes the “localhost” visible at the global scale. -https://book.getfoundry.sh/ +The platform includes: +- Issue Verification Layer: A lightweight, trust-based system (potentially leveraging proof-of-humanity or social attestations) for validating that submitted problems are grounded in lived experiences of specific communities. +- Navigator Dashboard: A UI for organizers and curators to browse, cluster, and remix community-sourced issues into complements to hackathon bounties, specific challenges, or even curated tracks, and ensure issue diversity and locality. +- Builder Interface: Participants can filter challenges by locality, theme, or affected group; form teams based on shared affinities; and explore historical issue–solution–adoption arcs. +- Reciprocity Layer: A system that tracks whether projects born from local issues return value to the communities of origin —whether through royalties, integrations, or future collaborations. -## Usage +Compass operates on the principle of cosmolocalism: making the local visible, meaningful, and influential in the global digital commons. It draws inspiration from concepts like “Sympoiesis”, derived from Greek words meaning "making-with", pointing at interdependence and co-creation. Ultimately, Compass acts as a kind of digital grandmother —grounded, wise, and quietly routing energy toward the heart of things. -### Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` - -### Deploy - -```shell -$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key -``` - -### Cast - -```shell -$ cast -``` - -### Help - -```shell -$ forge --help -$ anvil --help -$ cast --help -``` +The system is designed to be integrated into existing hackathon platforms (e.g., ETHGlobal, DoraHacks, Devfolio) as an opt-in module, and ultimately aims to become the protocol standard for fair issue-sourcing and community-aligned innovation at scale. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/app/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/api/suggest/route.ts b/app/api/suggest/route.ts new file mode 100644 index 0000000..2f4491f --- /dev/null +++ b/app/api/suggest/route.ts @@ -0,0 +1,47 @@ +import { OpenAI } from 'openai' +import { NextResponse } from 'next/server' +import sponsors from '@/lib/sponsors_tracks.json' + +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}) + +export async function POST(req: Request) { + const body = await req.json() + + const problems = body.problems || [] + const sponsorContext = JSON.stringify(sponsors, null, 2) + + const prompt = ` +You are an expert hackathon assistant. Based on these sponsor tracks and themes: + +${sponsorContext} + +And the following local community problems: + +${problems.map((p: any, i: number) => `${i + 1}. "${p.title}" - ${p.description}`).join('\n')} + +Suggest 1 creative, feasible hackathon project idea that aligns with 1-3 sponsors to help solve one of these problems. + +Format your response in markdown like this: + +# 🚀 [Project Title] + +## 🔍 Sponsor Track +[Sponsor names and tracks] + +## 💡 Project Idea +[2-3 sentence description of the project, its technical components, and how it helps solve the problem] +` + + const response = await openai.chat.completions.create({ + model: 'gpt-4', + messages: [{ role: 'user', content: prompt }], + temperature: 0.7, + }) + + const content = response.choices[0].message.content || '' + const suggestions = [content.trim()] + + return NextResponse.json({ suggestions }) +} \ No newline at end of file diff --git a/app/app/api/suggestions/route.ts b/app/app/api/suggestions/route.ts new file mode 100644 index 0000000..92b3e18 --- /dev/null +++ b/app/app/api/suggestions/route.ts @@ -0,0 +1,92 @@ +import { NextResponse } from 'next/server'; +import OpenAI from 'openai'; +import { ProblemBrief } from '@/lib/types'; + +// Initialize OpenAI client +const openai = new OpenAI({ + apiKey: process.env.OPENAI_API_KEY, +}); + +export async function POST(request: Request) { + let problemData: Partial = {}; + + try { + const problem: ProblemBrief = await request.json(); + problemData = problem; + + const prompt = `Generate 2 innovative hackathon project ideas to solve this community problem: + +PROBLEM CONTEXT +Title: ${problem.title} +Description: ${problem.description} + +For each solution, provide a response in exactly this format (including emojis): + +🚀 Project Title: [Concise, catchy title] + +💡 Description: [2-3 sentences explaining the core solution] + +🛠️ Tech Stack: +• [Key technology/framework 1] +• [Key technology/framework 2] +• [Key technology/framework 3] + +🎯 Impact: +• [Primary impact point] +• [Secondary impact point] + +--- + +Make each suggestion: +1. Technically feasible for a hackathon (2-3 days of coding) +2. Focused on real community impact +3. Leveraging modern technologies + +Generate exactly 2 suggestions with a clear separation between them.`; + + const completion = await openai.chat.completions.create({ + messages: [ + { + role: "system", + content: "You are a hackathon project ideation expert who specializes in generating innovative technical solutions for community problems. Your suggestions should be specific, technically detailed, and focused on real impact. Always maintain the exact format specified, including all emojis and bullet points." + }, + { + role: "user", + content: prompt + } + ], + model: "gpt-4-turbo-preview", + temperature: 0.7, + max_tokens: 1500, + }); + + const response = completion.choices[0]?.message?.content || ''; + // Split by '---' to separate suggestions, then filter out any empty strings + const suggestions = response.split('---') + .map(s => s.trim()) + .filter(s => s.length > 0); + + return NextResponse.json({ suggestions }); + } catch (error) { + console.error('Error generating suggestions:', error); + // Fallback suggestion in the same format as the prompt + return NextResponse.json( + { + error: 'Failed to generate suggestions', + suggestions: [`🚀 Project Title: "${problemData.title || 'Community'} Solution Platform" + +💡 Description: A community-driven platform that addresses ${problemData.description || 'local needs'} through innovative technology and local collaboration. + +🛠️ Tech Stack: +• Next.js for the frontend +• Node.js backend +• MongoDB for data storage + +🎯 Impact: +• Direct community engagement +• Measurable local improvement`] + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/app/favicon.ico b/app/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/app/favicon.ico differ diff --git a/app/app/globals.css b/app/app/globals.css new file mode 100644 index 0000000..dc98be7 --- /dev/null +++ b/app/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/app/layout.tsx b/app/app/layout.tsx new file mode 100644 index 0000000..f7fa87e --- /dev/null +++ b/app/app/layout.tsx @@ -0,0 +1,34 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/app/app/page.tsx b/app/app/page.tsx new file mode 100644 index 0000000..4768267 --- /dev/null +++ b/app/app/page.tsx @@ -0,0 +1,121 @@ +'use client' + +import { useState } from 'react' +import ProblemForm from '@/components/ProblemForm' +import ProblemCard from '@/components/ProblemCard' +import GeneratedSuggestionCard from '@/components/GeneratedSuggestionCard' +import { ProblemBrief } from '@/lib/types' +import { getAISuggestions } from '@/lib/ai' +import { Button } from '@/components/ui/button' +import { Sparkles } from 'lucide-react' + +export default function Home() { + const [problems, setProblems] = useState([]) + const [suggestions, setSuggestions] = useState([]) + const [loadingSuggestions, setLoadingSuggestions] = useState(false) + + const handleSubmit = (p: ProblemBrief) => { + setProblems([p, ...problems]) + } + + const generateSuggestions = async () => { + setLoadingSuggestions(true) + try { + const allSuggestions: string[] = [] + for (const problem of problems) { + const sug = await getAISuggestions(problem) + allSuggestions.push(...sug) + } + setSuggestions(allSuggestions) + } catch (err) { + console.error('Suggestion generation failed:', err) + } finally { + setLoadingSuggestions(false) + } + } + + return ( +
+
+
+

+ Building Local Impact +

+

+ Hackathons drive innovation, strengthen communities, and create solutions for local needs. + Join us in building a better future through technology. +

+
+ +
+
+
+ + + {problems.length > 0 && ( + + )} +
+
+ +
+ {problems.length > 0 && ( +
+

+ Submitted Problems +

+
+ {problems.map((p, idx) => ( +
+ +
+ ))} +
+
+ )} + + {suggestions.length > 0 && ( +
+

+ AI-Generated Project Ideas +

+
+ {suggestions.map((s, idx) => ( +
+ +
+ ))} +
+
+ )} + + {problems.length === 0 && ( +
+

+ Submit a community problem to get started +

+
+ )} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/components.json b/app/components.json new file mode 100644 index 0000000..335484f --- /dev/null +++ b/app/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/app/components/GeneratedSuggestionCard.tsx b/app/components/GeneratedSuggestionCard.tsx new file mode 100644 index 0000000..f05a79b --- /dev/null +++ b/app/components/GeneratedSuggestionCard.tsx @@ -0,0 +1,47 @@ +'use client' + +import { Card, CardContent } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { useState } from 'react' + +interface GeneratedSuggestionCardProps { + suggestion: string +} + +export default function GeneratedSuggestionCard({ suggestion }: GeneratedSuggestionCardProps) { + const [votes, setVotes] = useState({ build: 0, sponsor: 0, need: 0 }) + + return ( + + +

{suggestion}

+ +
+ + + + + +
+
+
+ ) +} \ No newline at end of file diff --git a/app/components/ProblemCard.tsx b/app/components/ProblemCard.tsx new file mode 100644 index 0000000..d532040 --- /dev/null +++ b/app/components/ProblemCard.tsx @@ -0,0 +1,50 @@ +'use client' + +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { ProblemBrief } from '@/lib/types' +import { Shield, CheckCircle2 } from 'lucide-react' + +interface ProblemCardProps { + problem: ProblemBrief +} + +export default function ProblemCard({ problem }: ProblemCardProps) { + return ( + + + + + {problem.title} + + {problem.verifiedDomain && ( + + + Verified Submitter + + )} + + + +

{problem.description}

+ +
+
+ +
+ Submitted by: + {problem.email} + {problem.verifiedDomain && ( +
+ + + Verified {problem.verifiedDomain} domain through Vlayer + +
+ )} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/components/ProblemForm.tsx b/app/components/ProblemForm.tsx new file mode 100644 index 0000000..224f78b --- /dev/null +++ b/app/components/ProblemForm.tsx @@ -0,0 +1,167 @@ +'use client' + +import { useState } from 'react' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import * as z from 'zod' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Textarea } from '@/components/ui/textarea' +import { FormData, ProblemBrief } from '@/lib/types' +import { Shield, CheckCircle2, Loader2 } from 'lucide-react' + +const formSchema = z.object({ + title: z.string().min(1, 'Title is required'), + description: z.string().min(1, 'Description is required'), + email: z.string().email('Invalid email address'), + emailVerified: z.boolean(), +}) + +interface ProblemFormProps { + onSubmit: (problem: ProblemBrief) => void +} + +export default function ProblemForm({ onSubmit }: ProblemFormProps) { + const [verifying, setVerifying] = useState(false) + + const { register, handleSubmit, formState: { errors }, setValue, watch } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + emailVerified: false, + }, + }) + + const email = watch('email') + const emailVerified = watch('emailVerified') + + const verifyEmail = async () => { + setVerifying(true) + // TODO: Implement actual email verification + await new Promise(resolve => setTimeout(resolve, 1500)) + setValue('emailVerified', true) + setValue('verifiedDomain', email.split('@')[1]) + setVerifying(false) + } + + const onSubmitForm = (data: FormData) => { + if (!data.emailVerified) return + + const problem: ProblemBrief = { + title: data.title, + description: data.description, + email: data.email, + verifiedDomain: email.split('@')[1], + } + + onSubmit(problem) + } + + return ( + + + + Submit a Local Problem + + + +
+
+ + {errors.title && ( +

{errors.title.message}

+ )} +
+ +
+