diff --git a/app/causes/page.tsx b/app/causes/page.tsx index 6fb13df6..9e1a02d9 100644 --- a/app/causes/page.tsx +++ b/app/causes/page.tsx @@ -2,15 +2,29 @@ import { createServerClient } from '@/db/supabase-server' import { FullCause, listFullCauses } from '@/db/cause' import Image from 'next/image' import Link from 'next/link' +import { FUNDER_SLUGS } from '@/utils/constants' export default async function CausesPage() { const supabase = createServerClient() const causesList = await listFullCauses(supabase) + const funders = causesList.filter((c) => FUNDER_SLUGS.includes(c.slug)) const prizes = causesList.filter((c) => c.prize) - const regularCauses = causesList.filter((c) => !c.prize) + const regularCauses = causesList.filter( + (c) => !c.prize && !FUNDER_SLUGS.includes(c.slug) + ) return (
-

+

Funders

+ + Other funders who partner with Manifund's common app + +
+ {funders.map((cause) => ( + + ))} +
+ +

Prize rounds

@@ -21,6 +35,7 @@ export default async function CausesPage() { ))}
+

Causes

diff --git a/app/create/create-project-form.tsx b/app/create/create-project-form.tsx index 0b1bbeb1..11f5d30b 100644 --- a/app/create/create-project-form.tsx +++ b/app/create/create-project-form.tsx @@ -23,8 +23,21 @@ import { HorizontalRadioGroup } from '@/components/radio-group' import { Checkbox } from '@/components/input' import { usePartialUpdater } from '@/hooks/user-partial-updater' import { ProjectParams } from '@/utils/upsert-project' +import questionBank from '../questions/questionBank.json' +import questionChoicesData from '../questions/questionChoices.json' +import { CheckCircleIcon } from '@heroicons/react/24/solid' +import { FUNDER_SLUGS } from '@/utils/constants' -const DESCRIPTION_OUTLINE = ` +interface QuestionsData { + id: string + question: string + description: string +} + +const questions = questionBank as QuestionsData[] +const questionChoices = questionChoicesData as { [key: string]: string[] } + +var DESCRIPTION_OUTLINE = `

Project summary


What are this project's goals and how will you achieve them?

@@ -38,6 +51,32 @@ const DESCRIPTION_OUTLINE = `

What other funding are you or your project getting?


` + +const addQuestionsToDescription = ( + selectedCauses: string[], + descriptionOutline: string +) => { + const addedQuestions = selectedCauses.reduce((set, cause) => { + const causeQuestionIds = questionChoices[cause] || [] + causeQuestionIds.forEach((questionId) => set.add(questionId)) + return set + }, new Set()) + + addedQuestions.forEach((questionId) => { + const question = questions.find((q) => q.id === questionId) + if (question) { + const formattedDescription = question.description.replace(/\n/g, '
') + descriptionOutline += ` +

${question.question}

+

${formattedDescription}

+
+ ` + } + }) + + return descriptionOutline +} + const DESCRIPTION_KEY = 'ProjectDescription' export function CreateProjectForm(props: { causesList: Cause[] }) { @@ -56,6 +95,8 @@ export function CreateProjectForm(props: { causesList: Cause[] }) { } ) const [isSubmitting, setIsSubmitting] = useState(false) + const [isLTFFSelected, setIsLTFFSelected] = useState(false) + const [isEAIFSelected, setIsEAIFSelected] = useState(false) const editor = useTextEditor(DESCRIPTION_OUTLINE, DESCRIPTION_KEY) const [madeChanges, setMadeChanges] = useState(false) @@ -76,19 +117,30 @@ export function CreateProjectForm(props: { causesList: Cause[] }) { 100, }) if (!madeChanges) { - editor?.commands.setContent( + let descriptionOutline = projectParams.selectedPrize?.project_description_outline ?? - DESCRIPTION_OUTLINE + DESCRIPTION_OUTLINE + + descriptionOutline = addQuestionsToDescription( + projectParams.selectedCauses.map((cause) => cause.slug), + descriptionOutline ) + + editor?.commands.setContent(descriptionOutline) setMadeChanges(false) } - }, [projectParams.selectedPrize]) + }, [projectParams.selectedPrize, projectParams.selectedCauses]) + const selectablePrizeCauses = causesList.filter( (cause) => cause.open && cause.prize ) const selectableCauses = causesList.filter( - (cause) => cause.open && !cause.prize + (cause) => cause.open && !cause.prize && !FUNDER_SLUGS.includes(cause.slug) + ) + const funderCauses = causesList.filter((cause) => + FUNDER_SLUGS.includes(cause.slug) ) + const minMinFunding = projectParams.selectedPrize?.cert_params ? projectParams.selectedPrize.cert_params.minMinFunding : 500 @@ -130,10 +182,10 @@ export function CreateProjectForm(props: { causesList: Cause[] }) { return (
-

Add a project

+

Propose a project

- + {/*

Select "a regular grant" by default. The other options are specific prizes that you can learn more about{' '} @@ -160,7 +212,7 @@ export function CreateProjectForm(props: { causesList: Cause[] }) { selectablePrizeCauses.map((cause) => [cause.slug, cause.title]) ), }} - /> + /> */}

+ Choose additional funders to review your project. Funders may ask + supplemental questions. +

+ + + updateProjectParams({ selectedCauses: newCauses }) + } + /> + {projectParams.selectedCauses.map((c) => c.slug).includes('ltff') && ( +
+

+ + Applying to{' '} + + Long-Term Future Fund + +

+

+ Funds people or projects that aim to improve the long-term future, + such as by reducing risks from artificial intelligence and + engineered pandemics +

+
+ )} + {projectParams.selectedCauses.map((c) => c.slug).includes('eaif') && ( +
+

+ + Applying to{' '} + + EA Infrastructure Fund + +

+

+ Funds organizations or people that aim to grow or improve the + effective altruism community +

+
+ )} + +

- Note that the editor offers formatting shortcuts{' '} - like Notion - {' '} - for hyperlinks, bullet points, headers, and more. + {' '} + for links, bullet points, headings, images and more.

@@ -327,9 +437,13 @@ export function CreateProjectForm(props: { causesList: Cause[] }) { - updateProjectParams({ selectedCauses: newCauses }) - } + setSelectedCauses={(newCauses: (MiniCause | undefined)[]) => + updateProjectParams({ + selectedCauses: newCauses.filter( + (cause): cause is MiniCause => !!cause + ), + }) + } //added a type assertion to filter out any undefined values from the newCauses array before updating the selectedCauses property. /> diff --git a/app/projects/[slug]/approve-app.tsx b/app/projects/[slug]/approve-app.tsx new file mode 100644 index 00000000..d92e7861 --- /dev/null +++ b/app/projects/[slug]/approve-app.tsx @@ -0,0 +1,69 @@ +import { Button } from '@/components/button' +import { Input } from '@/components/input' +import { Col } from '@/components/layout/col' +import { HorizontalRadioGroup } from '@/components/radio-group' +import { Project } from '@/db/project' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +type AppStage = 'proposal' | 'not funded' | 'active' | null + +// A component that allows a grantmaker to approve or reject a proposal, +// and to determine how much to fund it with, if approved. +export function JudgeApp(props: { project: Project }) { + const { project } = props + const router = useRouter() + const [appStage, setAppStage] = useState(null) + const [amount, setAppAmount] = useState(0) + return ( + +

Evaluate for LTFF

+ + { + setAppStage(value === 'approve' ? 'active' : 'not funded') + }} + /> + + {appStage === 'active' && ( + <> + Amount to fund ($) + setAppAmount(parseInt(e.target.value))} + placeholder="Amount" + /> + + )} + + + ) +} diff --git a/app/projects/[slug]/comments.tsx b/app/projects/[slug]/comments.tsx index f6aebdf2..1567a9d9 100644 --- a/app/projects/[slug]/comments.tsx +++ b/app/projects/[slug]/comments.tsx @@ -7,7 +7,7 @@ import { Project } from '@/db/project' import { ArrowUturnRightIcon } from '@heroicons/react/24/outline' import { PaperAirplaneIcon } from '@heroicons/react/24/solid' import { Row } from '@/components/layout/row' -import { IconButton } from '@/components/button' +import { Button, IconButton } from '@/components/button' import { useEffect, useState } from 'react' import { orderBy, sortBy } from 'lodash' import { Tooltip } from '@/components/tooltip' @@ -17,6 +17,7 @@ import { JSONContent } from '@tiptap/react' import clsx from 'clsx' import { clearLocalStorageItem } from '@/hooks/use-local-storage' import { Comment } from '@/components/comment' +import { Select } from '@/components/select' export function Comments(props: { project: Project diff --git a/app/projects/[slug]/project-tabs.tsx b/app/projects/[slug]/project-tabs.tsx index bb6b8ee0..315e2b2f 100644 --- a/app/projects/[slug]/project-tabs.tsx +++ b/app/projects/[slug]/project-tabs.tsx @@ -1,6 +1,6 @@ 'use client' import { Comments } from './comments' -import { FullProject, TOTAL_SHARES } from '@/db/project' +import { FullProject, ProjectStage, TOTAL_SHARES } from '@/db/project' import { Profile } from '@/db/profile' import { useSearchParams } from 'next/navigation' import { Bids } from './bids' @@ -15,6 +15,9 @@ import { uniq } from 'lodash' import { compareDesc } from 'date-fns' import { formatMoneyPrecise, formatPercent } from '@/utils/formatting' import { MarketTab } from '../market-tab' +import clsx from 'clsx' +import { Col } from '@/components/layout/col' +import { JudgeApp } from './approve-app' export function ProjectTabs(props: { project: FullProject @@ -110,6 +113,41 @@ export function ProjectTabs(props: { ), }) } + tabs.push({ + name: 'App status', + id: 'app-status', + count: 0, + display: ( + +

Application status

+

App status across various funders

+ +
+ Manifund +
+
+ EAIF{' '} + c.cause_slug === 'eaif') + ?.application_stage ?? null + } + /> +
+
+ LTFF{' '} + c.cause_slug === 'ltff') + ?.application_stage ?? null + } + /> +
+ + + + ), + }) if ( (project.stage === 'active' || project.stage === 'complete') && @@ -168,6 +206,29 @@ export function getShareholders(txns: TxnAndProfiles[]) { return shareholdersArray.filter((shareholder) => !!shareholder.profile) } +export function StageBadge(props: { stage: ProjectStage | null }) { + const { stage } = props + const colors = { + proposal: 'bg-blue-50 text-blue-700 ring-blue-700/10', + active: 'bg-green-50 text-green-700 ring-green-600/20', + complete: 'bg-purple-50 text-purple-700 ring-purple-700/10', + 'not funded': 'bg-red-50 text-red-700 ring-red-600/10', + hidden: 'bg-gray-50 text-gray-700 ring-gray-600/10', + draft: 'bg-gray-50 text-gray-700 ring-gray-600/10', + } + const color = colors[stage ?? 'hidden'] + return ( + + {stage ?? 'not applying'} + + ) +} + export function getCommenterContributions( comments: CommentAndProfile[], bids: BidAndProfile[], diff --git a/app/questions/questionBank.json b/app/questions/questionBank.json new file mode 100644 index 00000000..daeeebc6 --- /dev/null +++ b/app/questions/questionBank.json @@ -0,0 +1,22 @@ +[ + { + "id": "trackRecord", + "question": "Track record", + "description": "What is your track record for running projects of this kind?\n\nPlease give us an outline of previous successes and failures that would help us understand your (or your organization's) ability to execute this specific project. For example, campaigns you've implemented, products you've built, research you've published, or relevant professional and volunteering experience.\n\nAlternatively, what other kind of evidence do you have that the organization or project will succeed? If you are applying for a project that is related to a previous EA Funds grant, please elaborate on how your previous grant/project went." + }, + { + "id": "projectGoals", + "question": "Project goals", + "description": "What specific actions or steps might your project involve? What impact will this have on the world? What is your project's goal, how will you know if you've achieved it, and what is the path to impact? How does this relate to the goals of the fund(s) you are applying to?" + }, + { + "id": "destroyXRisk", + "question": "How are you going to destroy all x-risk", + "description": "joke answers only" + }, + { + "id": "10xEAMovement", + "question": "How are you going to 10x the ea movement", + "description": "cryptocurrency exchange?" + } +] \ No newline at end of file diff --git a/app/questions/questionChoices.json b/app/questions/questionChoices.json new file mode 100644 index 00000000..50eb3ee5 --- /dev/null +++ b/app/questions/questionChoices.json @@ -0,0 +1,12 @@ +{ + "ltff": [ + "trackRecord", + "projectGoals", + "destroyXRisk" + ], + "eaif": [ + "trackRecord", + "projectGoals", + "10xEAMovement" + ] +} \ No newline at end of file diff --git a/components/checkbox.tsx b/components/checkbox.tsx new file mode 100644 index 00000000..de091d62 --- /dev/null +++ b/components/checkbox.tsx @@ -0,0 +1,126 @@ +// not in use + +// import { RadioGroup } from '@headlessui/react' +// import clsx from 'clsx' +// import { Row } from './layout/row' + +// export function HorizontalRadioGroup(props: { +// value: string +// onChange: (value: string) => void +// options: { [key: string]: string } +// wide?: boolean +// }) { +// const { value, onChange, options, wide } = props +// return ( +// +// +// {Object.entries(options).map(([type, label]) => ( +// +// clsx( +// 'cursor-pointer focus:outline-none', +// checked +// ? 'bg-orange-500 text-white hover:bg-orange-600' +// : 'bg-white text-gray-900', +// 'flex items-center justify-center rounded-md px-3 py-3 text-sm font-semibold' +// ) +// } +// > +// {label} +// +// ))} +// +// +// ) +// } + + +// // tailwind.config.js +// module.exports = { +// // ... +// plugins: [ +// // ... +// require('@tailwindcss/forms'), +// ], +// } +// ``` +// */ + + +export default function Checkbox() { + return ( +
+ Notifications +
+
+
+ +
+
+ +

+ Get notified when someones posts a comment on a posting. +

+
+
+
+
+ +
+
+ +

+ Get notified when a candidate applies for a job. +

+
+
+
+
+ +
+
+ +

+ Get notified when a candidate accepts or rejects an offer. +

+
+
+
+
+ ) +} diff --git a/db/database.types.ts b/db/database.types.ts index 8b6ac26a..1edc95ef 100644 --- a/db/database.types.ts +++ b/db/database.types.ts @@ -342,14 +342,21 @@ export type Database = { } project_causes: { Row: { + application_stage: Database["public"]["Enums"]["project_stage"] | null cause_slug: string project_id: string } Insert: { + application_stage?: + | Database["public"]["Enums"]["project_stage"] + | null cause_slug: string project_id: string } Update: { + application_stage?: + | Database["public"]["Enums"]["project_stage"] + | null cause_slug?: string project_id?: string } diff --git a/db/project.ts b/db/project.ts index c848facc..57e7dafe 100644 --- a/db/project.ts +++ b/db/project.ts @@ -25,9 +25,11 @@ export type FullProject = Project & { profiles: Profile } & { project_transfers: ProjectTransfer[] } & { project_votes: ProjectVote[] } & { causes: MiniCause[] } & { project_follows: ProjectFollow[] -} +} & { project_causes: ProjectCause[] } export type MiniProject = Project & { profiles: Profile } & { txns: Txn[] } export const TOTAL_SHARES = 10_000_000 +export type ProjectStage = Database['public']['Enums']['project_stage'] +export type ProjectCause = Database['public']['Tables']['project_causes']['Row'] export async function getProjectBySlug(supabase: SupabaseClient, slug: string) { const { data, error } = await supabase @@ -101,7 +103,7 @@ export async function getFullProjectBySlug( const { data } = await supabase .from('projects') .select( - '*, profiles!projects_creator_fkey(*), bids(*), txns(*), comments(*), rounds(*), project_transfers(*), project_votes(*), project_follows(follower_id), causes(title, slug)' + '*, profiles!projects_creator_fkey(*), bids(*), txns(*), comments(*), rounds(*), project_transfers(*), project_votes(*), project_follows(follower_id), causes(title, slug), project_causes(*)' ) .eq('slug', slug) .throwOnError() diff --git a/pages/api/judge-app.ts b/pages/api/judge-app.ts new file mode 100644 index 00000000..ec17d4cd --- /dev/null +++ b/pages/api/judge-app.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server' +import { createEdgeClient } from './_db' +import { getProjectById, updateProjectStage } from '@/db/project' +import { JSONContent } from '@tiptap/core' +import { sendComment } from '@/db/comment' + +export const config = { + runtime: 'edge', + regions: ['sfo1'], + // From https://github.com/lodash/lodash/issues/5525 + unstable_allowDynamic: [ + '**/node_modules/lodash/_root.js', // Use a glob to allow anything in the function-bind 3rd party module + ], +} + +type judgeAppProps = { + projectId: string + causeSlug: string + decision: 'approve' | 'reject' + funding: number +} + +export default async function handler(req: NextRequest) { + const { projectId, causeSlug, decision, funding } = + (await req.json()) as judgeAppProps + const supabase = createEdgeClient(req) + + // Turn off auth checks for hackathon + // const resp = await supabase.auth.getUser() + // const user = resp.data.user + // const project = await getProjectById(supabase, projectId) + // if (!user || user.id !== project.creator) return NextResponse.error() + + // Update the project cause to include the decision and funding amount + const { data, error } = await supabase + .from('project_causes') + .update({ + application_stage: decision === 'approve' ? 'active' : 'not funded', + }) + .eq('project_id', projectId) + .eq('cause_slug', causeSlug) + + return NextResponse.json('success') +} diff --git a/seed.sql b/seed.sql index 4a3e4c92..8c2a259b 100644 --- a/seed.sql +++ b/seed.sql @@ -14,6 +14,7 @@ create table public.profiles ( long_description jsonb, regranter_status boolean not null, stripe_connect_id text, + managed_causes jsonb, primary key (id) ); @@ -96,6 +97,7 @@ create table if not exists public.projects ( approved boolean, signed_agreement boolean not null default false, markets jsonb, + private bool, primary key (id) ); @@ -110,7 +112,7 @@ UPDATE CREATE POLICY "Enable read access for all users" ON "public"."projects" AS PERMISSIVE FOR SELECT - TO public USING (true); + TO public USING (private = false); CREATE POLICY "Enable update for austin based on email" ON "public"."projects" AS PERMISSIVE FOR UPDATE @@ -154,6 +156,24 @@ INSERT AND auth.uid() :: text = (storage.foldername(name)) [1] ); +CREATE POLICY "Allow user to see own private records" +ON "public"."projects" +TO authenticated +USING ((( SELECT auth.uid() AS uid) = creator) +); + +CREATE policy "Allow cause managers to see cause grants" +ON "public"."projects" +TO public +USING ( + ((private = false) OR (id IN ( SELECT projects.id + FROM project_causes + WHERE (project_causes.cause_slug IN ( SELECT jsonb_array_elements_text(profiles.managed_causes) AS jsonb_array_elements_text + FROM profiles + WHERE (profiles.id = auth.uid())))))) +); + + -- -- -- @@ -368,6 +388,7 @@ CREATE TABLE public.project_causes ( id int8 NOT NULL, project_id uuid NOT NULL REFERENCES public.projects(id) ON DELETE CASCADE, tag_slug text NOT NULL REFERENCES public.causes(slug) ON DELETE CASCADE, + application_stage project_stage DEFAULT null, PRIMARY KEY (id) ); diff --git a/utils/constants.ts b/utils/constants.ts index 83bf1643..4d90d108 100644 --- a/utils/constants.ts +++ b/utils/constants.ts @@ -98,3 +98,5 @@ export function isCharitableDeposit(txnId: string) { } export const CURRENT_AGREEMENT_VERSION = 3 + +export const FUNDER_SLUGS = ['ltff', 'eaif']